diff options
author | Mario Vavti <mario@mariovavti.com> | 2017-05-31 09:56:35 +0200 |
---|---|---|
committer | Mario Vavti <mario@mariovavti.com> | 2017-05-31 09:56:35 +0200 |
commit | 47d55694a4c84b6c12c0db61a69bcac8b671b20e (patch) | |
tree | b15e96f4ea67e2214a66a9d28dafaf53d25b98ec /vendor | |
parent | 087f9784e3c5a860ed2b86e7f9e8e9f312038546 (diff) | |
parent | f0e615dee529e031663576286345141ad2996974 (diff) | |
download | volse-hubzilla-47d55694a4c84b6c12c0db61a69bcac8b671b20e.tar.gz volse-hubzilla-47d55694a4c84b6c12c0db61a69bcac8b671b20e.tar.bz2 volse-hubzilla-47d55694a4c84b6c12c0db61a69bcac8b671b20e.zip |
Merge branch '2.4RC'2.4
Diffstat (limited to 'vendor')
626 files changed, 87964 insertions, 24 deletions
diff --git a/vendor/autoload.php b/vendor/autoload.php index 063a1b7e1..8b4926c3d 100644 --- a/vendor/autoload.php +++ b/vendor/autoload.php @@ -2,6 +2,6 @@ // autoload.php @generated by Composer -require_once __DIR__ . '/composer' . '/autoload_real.php'; +require_once __DIR__ . '/composer/autoload_real.php'; return ComposerAutoloaderInit7b34d7e50a62201ec5d5e526a5b8b35d::getLoader(); diff --git a/vendor/bshaffer/oauth2-server-php/CHANGELOG.md b/vendor/bshaffer/oauth2-server-php/CHANGELOG.md new file mode 100644 index 000000000..4fddd72c9 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/CHANGELOG.md @@ -0,0 +1,182 @@ +CHANGELOG for 1.x +================= + +This changelog references the relevant changes (bug and security fixes) done +in 1.x minor versions. + +To see the files changed for a given bug, go to https://github.com/bshaffer/oauth2-server-php/issues/### where ### is the bug number +To get the diff between two versions, go to https://github.com/bshaffer/oauth2-server-php/compare/v1.0...v1.1 +To get the diff for a specific change, go to https://github.com/bshaffer/oauth2-server-php/commit/XXX where XXX is the change hash + +* 1.9.0 (2016-01-06) + + PR: https://github.com/bshaffer/oauth2-server-php/pull/788 + + * bug #645 - Allow null for client_secret + * bug #651 - Fix bug in isPublicClient of Cassandra Storage + * bug #670 - Bug in client's scope restriction + * bug #672 - Implemented method to override the password hashing algorithm + * bug #698 - Fix Token Response's Content-Type to application/json + * bug #729 - Ensures unsetAccessToken and unsetRefreshToken return a bool + * bug #749 - Fix UserClaims for CodeIdToken + * bug #784 - RFC6750 compatibility + * bug #776 - Fix "redirect_uri_mismatch" for URIs with encoded characters + * bug #759 - no access token supplied to resource controller results in empty request body + * bug #773 - Use OpenSSL random method before attempting Mcrypt's. + * bug #790 - Add mongo db + +* 1.8.0 (2015-09-18) + + PR: https://github.com/bshaffer/oauth2-server-php/pull/643 + + * bug #594 - adds jti + * bug #598 - fixes lifetime configurations for JWTs + * bug #634 - fixes travis builds, upgrade to containers + * bug #586 - support for revoking tokens + * bug #636 - Adds FirebaseJWT bridge + * bug #639 - Mongo HHVM compatibility + +* 1.7.0 (2015-04-23) + + PR: https://github.com/bshaffer/oauth2-server-php/pull/572 + + * bug #500 - PDO fetch mode changed from FETCH_BOTH to FETCH_ASSOC + * bug #508 - Case insensitive for Bearer token header name ba716d4 + * bug #512 - validateRedirectUri is now public + * bug #530 - Add PublicKeyInterface, UserClaimsInterface to Cassandra Storage + * bug #505 - DynamoDB storage fixes + * bug #556 - adds "code id_token" return type to openid connect + * bug #563 - Include "issuer" config key for JwtAccessToken + * bug #564 - Fixes JWT vulnerability + * bug #571 - Added unset_refresh_token_after_use option + +* 1.6 (2015-01-16) + + PR: https://github.com/bshaffer/oauth2-server-php/pull/496 + + * bug 437 - renames CryptoToken to JwtAccessToken / use_crypto_tokens to use_jwt_access_tokens + * bug 447 - Adds a Couchbase storage implementation + * bug 460 - Rename JWT claims to match spec + * bug 470 - order does not matter for multi-valued response types + * bug 471 - Make validateAuthorizeRequest available for POST in addition to GET + * bug 475 - Adds JTI table definitiion + * bug 481 - better randomness for generating access tokens + * bug 480 - Use hash_equals() for signature verification (prevents remote timing attacks) + * bugs 489, 491, 498 - misc other fixes + +* 1.5 (2014-08-27) + + PR: https://github.com/bshaffer/oauth2-server-php/pull/446 + + * bug #399 - Add DynamoDB Support + * bug #404 - renamed error name for malformed/expired tokens + * bug #412 - Openid connect: fixes for claims with more than one scope / Add support for the prompt parameter ('consent' and 'none') + * bug #411 - fixes xml output + * bug #413 - fixes invalid format error + * bug #401 - fixes code standards / whitespace + * bug #354 - bundles PDO SQL with the library + * [BC] bug #397 - refresh tokens should not be encrypted + * bug #423 - makes "scope" optional for refresh token storage + +* 1.4 (2014-06-12) + + PR: https://github.com/bshaffer/oauth2-server-php/pull/392 + + * bug #189 Storage\PDO - allows DSN string in constructor + * bug #233 Bearer Tokens - allows token in request body for PUT requests + * bug #346 Fixes open_basedir warning + * bug #351 Adds OpenID Connect support + * bug #355 Adds php 5.6 and HHVM to travis.ci testing + * [BC] bug #358 Adds `getQuerystringIdentifier()` to the GrantType interface + * bug #363 Encryption\JWT - Allows for subclassing JWT Headers + * bug #349 Bearer Tokens - adds requestHasToken method for when access tokens are optional + * bug #301 Encryption\JWT - fixes urlSafeB64Encode(): ensures newlines are replaced as expected + * bug #323 ResourceController - client_id is no longer required to be returned when calling getAccessToken + * bug #367 Storage\PDO - adds Postgres support + * bug #368 Access Tokens - use mcrypt_create_iv or openssl_random_pseudo_bytes to create token string + * bug #376 Request - allows case insensitive headers + * bug #384 Storage\PDO - can pass in PDO options in constructor of PDO storage + * misc fixes #361, #292, #373, #374, #379, #396 +* 1.3 (2014-02-27) + + PR: https://github.com/bshaffer/oauth2-server-php/pull/325 + + * bug #311 adds cassandra storage + * bug #298 fixes response code for user credentials grant type + * bug #318 adds 'use_crypto_tokens' config to Server class for better DX + * [BC] bug #320 pass client_id to getDefaultScope + * bug #324 better feedback when running tests + * bug #335 adds support for non-expiring refresh tokens + * bug #333 fixes Pdo storage for getClientKey + * bug #336 fixes Redis storage for expireAuthorizationCode + +* 1.3 (2014-02-27) + + PR: https://github.com/bshaffer/oauth2-server-php/pull/325 + + * bug #311 adds cassandra storage + * bug #298 fixes response code for user credentials grant type + * bug #318 adds 'use_crypto_tokens' config to Server class for better DX + * bug #320 pass client_id to getDefaultScope + * bug #324 better feedback when running tests + * bug #335 adds support for non-expiring refresh tokens + * bug #333 fixes Pdo storage for getClientKey + * bug #336 fixes Redis storage for expireAuthorizationCode + +* 1.2 (2014-01-03) + + PR: https://github.com/bshaffer/oauth2-server-php/pull/288 + + * bug #285 changed response header from 200 to 401 when empty token received + * bug #286 adds documentation and links to spec for not including error messages when no token is supplied + * bug #280 ensures PHP warnings do not get thrown as a result of an invalid argument to $jwt->decode() + * bug #279 predis wrong number of arguments + * bug #277 Securing JS WebApp client secret w/ password grant type + +* 1.1 (2013-12-17) + + PR: https://github.com/bshaffer/oauth2-server-php/pull/276 + + * bug #278 adds refresh token configuration to Server class + * bug #274 Supplying a null client_id and client_secret grants API access + * bug #244 [MongoStorage] More detailed implementation info + * bug #268 Implement jti for JWT Bearer tokens to prevent replay attacks. + * bug #266 Removing unused argument to getAccessTokenData + * bug #247 Make Bearer token type consistent + * bug #253 Fixing CryptoToken refresh token lifetime + * bug #246 refactors public key logic to be more intuitive + * bug #245 adds support for JSON crypto tokens + * bug #230 Remove unused columns in oauth_clients + * bug #215 makes Redis Scope Storage obey the same paradigm as PDO + * bug #228 removes scope group + * bug #227 squelches open basedir restriction error + * bug #223 Updated docblocks for RefreshTokenInterface.php + * bug #224 Adds protected properties + * bug #217 Implement ScopeInterface for PDO, Redis + +* 1.0 (2013-08-12) + + * bug #203 Add redirect\_status_code config param for AuthorizeController + * bug #205 ensures unnecessary ? is not set when ** bug + * bug #204 Fixed call to LogicException + * bug #202 Add explode to checkRestrictedGrant in PDO Storage + * bug #197 adds support for 'false' default scope ** bug + * bug #192 reference errors and adds tests + * bug #194 makes some appropriate properties ** bug + * bug #191 passes config to HttpBasic + * bug #190 validates client credentials before ** bug + * bug #171 Fix wrong redirect following authorization step + * bug #187 client_id is now passed to getDefaultScope(). + * bug #176 Require refresh_token in getRefreshToken response + * bug #174 make user\_id not required for refresh_token grant + * bug #173 Duplication in JwtBearer Grant + * bug #168 user\_id not required for authorization_code grant + * bug #133 hardens default security for user object + * bug #163 allows redirect\_uri on authorization_code to be NULL in docs example + * bug #162 adds getToken on ResourceController for convenience + * bug #161 fixes fatal error + * bug #163 Invalid redirect_uri handling + * bug #156 user\_id in OAuth2\_Storage_AuthorizationCodeInterface::getAuthorizationCode() response + * bug #157 Fix for extending access and refresh tokens + * bug #154 ResponseInterface: getParameter method is used in the library but not defined in the interface + * bug #148 Add more detail to examples in Readme.md diff --git a/vendor/bshaffer/oauth2-server-php/LICENSE b/vendor/bshaffer/oauth2-server-php/LICENSE new file mode 100644 index 000000000..d7ece8467 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2014 Brent Shaffer + +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. diff --git a/vendor/bshaffer/oauth2-server-php/README.md b/vendor/bshaffer/oauth2-server-php/README.md new file mode 100644 index 000000000..4ceda6cf9 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/README.md @@ -0,0 +1,8 @@ +oauth2-server-php +================= + +[![Build Status](https://travis-ci.org/bshaffer/oauth2-server-php.svg?branch=develop)](https://travis-ci.org/bshaffer/oauth2-server-php) + +[![Total Downloads](https://poser.pugx.org/bshaffer/oauth2-server-php/downloads.png)](https://packagist.org/packages/bshaffer/oauth2-server-php) + +View the [complete documentation](http://bshaffer.github.io/oauth2-server-php-docs/)
\ No newline at end of file diff --git a/vendor/bshaffer/oauth2-server-php/composer.json b/vendor/bshaffer/oauth2-server-php/composer.json new file mode 100644 index 000000000..561699f5e --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/composer.json @@ -0,0 +1,34 @@ +{ + "name": "bshaffer/oauth2-server-php", + "description":"OAuth2 Server for PHP", + "keywords":["oauth","oauth2","auth"], + "type":"library", + "license":"MIT", + "authors":[ + { + "name":"Brent Shaffer", + "email": "bshafs@gmail.com", + "homepage":"http://brentertainment.com" + } + ], + "homepage": "http://github.com/bshaffer/oauth2-server-php", + "autoload": { + "psr-0": { "OAuth2": "src/" } + }, + "require":{ + "php":">=5.3.9" + }, + "require-dev": { + "aws/aws-sdk-php": "~2.8", + "firebase/php-jwt": "~2.2", + "predis/predis": "dev-master", + "thobbs/phpcassa": "dev-master", + "mongodb/mongodb": "^1.1" + }, + "suggest": { + "predis/predis": "Required to use Redis storage", + "thobbs/phpcassa": "Required to use Cassandra storage", + "aws/aws-sdk-php": "~2.8 is required to use DynamoDB storage", + "firebase/php-jwt": "~1.1 is required to use MondoDB storage" + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Autoloader.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Autoloader.php new file mode 100644 index 000000000..ecfb6ba75 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Autoloader.php @@ -0,0 +1,48 @@ +<?php + +namespace OAuth2; + +/** + * Autoloads OAuth2 classes + * + * @author Brent Shaffer <bshafs at gmail dot com> + * @license MIT License + */ +class Autoloader +{ + private $dir; + + public function __construct($dir = null) + { + if (is_null($dir)) { + $dir = dirname(__FILE__).'/..'; + } + $this->dir = $dir; + } + /** + * Registers OAuth2\Autoloader as an SPL autoloader. + */ + public static function register($dir = null) + { + ini_set('unserialize_callback_func', 'spl_autoload_call'); + spl_autoload_register(array(new self($dir), 'autoload')); + } + + /** + * Handles autoloading of classes. + * + * @param string $class A class name. + * + * @return boolean Returns true if the class has been loaded + */ + public function autoload($class) + { + if (0 !== strpos($class, 'OAuth2')) { + return; + } + + if (file_exists($file = $this->dir.'/'.str_replace('\\', '/', $class).'.php')) { + require $file; + } + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php new file mode 100644 index 000000000..29c7171b5 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php @@ -0,0 +1,15 @@ +<?php + +namespace OAuth2\ClientAssertionType; + +use OAuth2\RequestInterface; +use OAuth2\ResponseInterface; + +/** + * Interface for all OAuth2 Client Assertion Types + */ +interface ClientAssertionTypeInterface +{ + public function validateRequest(RequestInterface $request, ResponseInterface $response); + public function getClientId(); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/HttpBasic.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/HttpBasic.php new file mode 100644 index 000000000..0ecb7e18d --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/HttpBasic.php @@ -0,0 +1,123 @@ +<?php + +namespace OAuth2\ClientAssertionType; + +use OAuth2\Storage\ClientCredentialsInterface; +use OAuth2\RequestInterface; +use OAuth2\ResponseInterface; + +/** + * Validate a client via Http Basic authentication + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +class HttpBasic implements ClientAssertionTypeInterface +{ + private $clientData; + + protected $storage; + protected $config; + + /** + * @param OAuth2\Storage\ClientCredentialsInterface $clientStorage REQUIRED Storage class for retrieving client credentials information + * @param array $config OPTIONAL Configuration options for the server + * <code> + * $config = array( + * 'allow_credentials_in_request_body' => true, // whether to look for credentials in the POST body in addition to the Authorize HTTP Header + * 'allow_public_clients' => true // if true, "public clients" (clients without a secret) may be authenticated + * ); + * </code> + */ + public function __construct(ClientCredentialsInterface $storage, array $config = array()) + { + $this->storage = $storage; + $this->config = array_merge(array( + 'allow_credentials_in_request_body' => true, + 'allow_public_clients' => true, + ), $config); + } + + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$clientData = $this->getClientCredentials($request, $response)) { + return false; + } + + if (!isset($clientData['client_id'])) { + throw new \LogicException('the clientData array must have "client_id" set'); + } + + if (!isset($clientData['client_secret']) || $clientData['client_secret'] == '') { + if (!$this->config['allow_public_clients']) { + $response->setError(400, 'invalid_client', 'client credentials are required'); + + return false; + } + + if (!$this->storage->isPublicClient($clientData['client_id'])) { + $response->setError(400, 'invalid_client', 'This client is invalid or must authenticate using a client secret'); + + return false; + } + } elseif ($this->storage->checkClientCredentials($clientData['client_id'], $clientData['client_secret']) === false) { + $response->setError(400, 'invalid_client', 'The client credentials are invalid'); + + return false; + } + + $this->clientData = $clientData; + + return true; + } + + public function getClientId() + { + return $this->clientData['client_id']; + } + + /** + * Internal function used to get the client credentials from HTTP basic + * auth or POST data. + * + * According to the spec (draft 20), the client_id can be provided in + * the Basic Authorization header (recommended) or via GET/POST. + * + * @return + * A list containing the client identifier and password, for example + * @code + * return array( + * "client_id" => CLIENT_ID, // REQUIRED the client id + * "client_secret" => CLIENT_SECRET, // OPTIONAL the client secret (may be omitted for public clients) + * ); + * @endcode + * + * @see http://tools.ietf.org/html/rfc6749#section-2.3.1 + * + * @ingroup oauth2_section_2 + */ + public function getClientCredentials(RequestInterface $request, ResponseInterface $response = null) + { + if (!is_null($request->headers('PHP_AUTH_USER')) && !is_null($request->headers('PHP_AUTH_PW'))) { + return array('client_id' => $request->headers('PHP_AUTH_USER'), 'client_secret' => $request->headers('PHP_AUTH_PW')); + } + + if ($this->config['allow_credentials_in_request_body']) { + // Using POST for HttpBasic authorization is not recommended, but is supported by specification + if (!is_null($request->request('client_id'))) { + /** + * client_secret can be null if the client's password is an empty string + * @see http://tools.ietf.org/html/rfc6749#section-2.3.1 + */ + + return array('client_id' => $request->request('client_id'), 'client_secret' => $request->request('client_secret')); + } + } + + if ($response) { + $message = $this->config['allow_credentials_in_request_body'] ? ' or body' : ''; + $response->setError(400, 'invalid_client', 'Client credentials were not found in the headers'.$message); + } + + return null; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeController.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeController.php new file mode 100644 index 000000000..ea7f54a87 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeController.php @@ -0,0 +1,393 @@ +<?php + +namespace OAuth2\Controller; + +use OAuth2\Storage\ClientInterface; +use OAuth2\ScopeInterface; +use OAuth2\RequestInterface; +use OAuth2\ResponseInterface; +use OAuth2\Scope; + +/** + * @see OAuth2\Controller\AuthorizeControllerInterface + */ +class AuthorizeController implements AuthorizeControllerInterface +{ + private $scope; + private $state; + private $client_id; + private $redirect_uri; + private $response_type; + + protected $clientStorage; + protected $responseTypes; + protected $config; + protected $scopeUtil; + + /** + * @param OAuth2\Storage\ClientInterface $clientStorage REQUIRED Instance of OAuth2\Storage\ClientInterface to retrieve client information + * @param array $responseTypes OPTIONAL Array of OAuth2\ResponseType\ResponseTypeInterface objects. Valid array + * keys are "code" and "token" + * @param array $config OPTIONAL Configuration options for the server + * <code> + * $config = array( + * 'allow_implicit' => false, // if the controller should allow the "implicit" grant type + * 'enforce_state' => true // if the controller should require the "state" parameter + * 'require_exact_redirect_uri' => true, // if the controller should require an exact match on the "redirect_uri" parameter + * 'redirect_status_code' => 302, // HTTP status code to use for redirect responses + * ); + * </code> + * @param OAuth2\ScopeInterface $scopeUtil OPTIONAL Instance of OAuth2\ScopeInterface to validate the requested scope + */ + public function __construct(ClientInterface $clientStorage, array $responseTypes = array(), array $config = array(), ScopeInterface $scopeUtil = null) + { + $this->clientStorage = $clientStorage; + $this->responseTypes = $responseTypes; + $this->config = array_merge(array( + 'allow_implicit' => false, + 'enforce_state' => true, + 'require_exact_redirect_uri' => true, + 'redirect_status_code' => 302, + ), $config); + + if (is_null($scopeUtil)) { + $scopeUtil = new Scope(); + } + $this->scopeUtil = $scopeUtil; + } + + public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null) + { + if (!is_bool($is_authorized)) { + throw new \InvalidArgumentException('Argument "is_authorized" must be a boolean. This method must know if the user has granted access to the client.'); + } + + // We repeat this, because we need to re-validate. The request could be POSTed + // by a 3rd-party (because we are not internally enforcing NONCEs, etc) + if (!$this->validateAuthorizeRequest($request, $response)) { + return; + } + + // If no redirect_uri is passed in the request, use client's registered one + if (empty($this->redirect_uri)) { + $clientData = $this->clientStorage->getClientDetails($this->client_id); + $registered_redirect_uri = $clientData['redirect_uri']; + } + + // the user declined access to the client's application + if ($is_authorized === false) { + $redirect_uri = $this->redirect_uri ?: $registered_redirect_uri; + $this->setNotAuthorizedResponse($request, $response, $redirect_uri, $user_id); + + return; + } + + // build the parameters to set in the redirect URI + if (!$params = $this->buildAuthorizeParameters($request, $response, $user_id)) { + return; + } + + $authResult = $this->responseTypes[$this->response_type]->getAuthorizeResponse($params, $user_id); + + list($redirect_uri, $uri_params) = $authResult; + + if (empty($redirect_uri) && !empty($registered_redirect_uri)) { + $redirect_uri = $registered_redirect_uri; + } + + $uri = $this->buildUri($redirect_uri, $uri_params); + + // return redirect response + $response->setRedirect($this->config['redirect_status_code'], $uri); + } + + protected function setNotAuthorizedResponse(RequestInterface $request, ResponseInterface $response, $redirect_uri, $user_id = null) + { + $error = 'access_denied'; + $error_message = 'The user denied access to your application'; + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $this->state, $error, $error_message); + } + + /* + * We have made this protected so this class can be extended to add/modify + * these parameters + */ + protected function buildAuthorizeParameters($request, $response, $user_id) + { + // @TODO: we should be explicit with this in the future + $params = array( + 'scope' => $this->scope, + 'state' => $this->state, + 'client_id' => $this->client_id, + 'redirect_uri' => $this->redirect_uri, + 'response_type' => $this->response_type, + ); + + return $params; + } + + /** + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response) + { + // Make sure a valid client id was supplied (we can not redirect because we were unable to verify the URI) + if (!$client_id = $request->query('client_id', $request->request('client_id'))) { + // We don't have a good URI to use + $response->setError(400, 'invalid_client', "No client id supplied"); + + return false; + } + + // Get client details + if (!$clientData = $this->clientStorage->getClientDetails($client_id)) { + $response->setError(400, 'invalid_client', 'The client id supplied is invalid'); + + return false; + } + + $registered_redirect_uri = isset($clientData['redirect_uri']) ? $clientData['redirect_uri'] : ''; + + // Make sure a valid redirect_uri was supplied. If specified, it must match the clientData URI. + // @see http://tools.ietf.org/html/rfc6749#section-3.1.2 + // @see http://tools.ietf.org/html/rfc6749#section-4.1.2.1 + // @see http://tools.ietf.org/html/rfc6749#section-4.2.2.1 + if ($supplied_redirect_uri = $request->query('redirect_uri', $request->request('redirect_uri'))) { + // validate there is no fragment supplied + $parts = parse_url($supplied_redirect_uri); + if (isset($parts['fragment']) && $parts['fragment']) { + $response->setError(400, 'invalid_uri', 'The redirect URI must not contain a fragment'); + + return false; + } + + // validate against the registered redirect uri(s) if available + if ($registered_redirect_uri && !$this->validateRedirectUri($supplied_redirect_uri, $registered_redirect_uri)) { + $response->setError(400, 'redirect_uri_mismatch', 'The redirect URI provided is missing or does not match', '#section-3.1.2'); + + return false; + } + $redirect_uri = $supplied_redirect_uri; + } else { + // use the registered redirect_uri if none has been supplied, if possible + if (!$registered_redirect_uri) { + $response->setError(400, 'invalid_uri', 'No redirect URI was supplied or stored'); + + return false; + } + + if (count(explode(' ', $registered_redirect_uri)) > 1) { + $response->setError(400, 'invalid_uri', 'A redirect URI must be supplied when multiple redirect URIs are registered', '#section-3.1.2.3'); + + return false; + } + $redirect_uri = $registered_redirect_uri; + } + + // Select the redirect URI + $response_type = $request->query('response_type', $request->request('response_type')); + + // for multiple-valued response types - make them alphabetical + if (false !== strpos($response_type, ' ')) { + $types = explode(' ', $response_type); + sort($types); + $response_type = ltrim(implode(' ', $types)); + } + + $state = $request->query('state', $request->request('state')); + + // type and client_id are required + if (!$response_type || !in_array($response_type, $this->getValidResponseTypes())) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_request', 'Invalid or missing response type', null); + + return false; + } + + if ($response_type == self::RESPONSE_TYPE_AUTHORIZATION_CODE) { + if (!isset($this->responseTypes['code'])) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unsupported_response_type', 'authorization code grant type not supported', null); + + return false; + } + if (!$this->clientStorage->checkRestrictedGrantType($client_id, 'authorization_code')) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unauthorized_client', 'The grant type is unauthorized for this client_id', null); + + return false; + } + if ($this->responseTypes['code']->enforceRedirect() && !$redirect_uri) { + $response->setError(400, 'redirect_uri_mismatch', 'The redirect URI is mandatory and was not supplied'); + + return false; + } + } else { + if (!$this->config['allow_implicit']) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unsupported_response_type', 'implicit grant type not supported', null); + + return false; + } + if (!$this->clientStorage->checkRestrictedGrantType($client_id, 'implicit')) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unauthorized_client', 'The grant type is unauthorized for this client_id', null); + + return false; + } + } + + // validate requested scope if it exists + $requestedScope = $this->scopeUtil->getScopeFromRequest($request); + + if ($requestedScope) { + // restrict scope by client specific scope if applicable, + // otherwise verify the scope exists + $clientScope = $this->clientStorage->getClientScope($client_id); + if ((empty($clientScope) && !$this->scopeUtil->scopeExists($requestedScope)) + || (!empty($clientScope) && !$this->scopeUtil->checkScope($requestedScope, $clientScope))) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_scope', 'An unsupported scope was requested', null); + + return false; + } + } else { + // use a globally-defined default scope + $defaultScope = $this->scopeUtil->getDefaultScope($client_id); + + if (false === $defaultScope) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_client', 'This application requires you specify a scope parameter', null); + + return false; + } + + $requestedScope = $defaultScope; + } + + // Validate state parameter exists (if configured to enforce this) + if ($this->config['enforce_state'] && !$state) { + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, null, 'invalid_request', 'The state parameter is required'); + + return false; + } + + // save the input data and return true + $this->scope = $requestedScope; + $this->state = $state; + $this->client_id = $client_id; + // Only save the SUPPLIED redirect URI (@see http://tools.ietf.org/html/rfc6749#section-4.1.3) + $this->redirect_uri = $supplied_redirect_uri; + $this->response_type = $response_type; + + return true; + } + + /** + * Build the absolute URI based on supplied URI and parameters. + * + * @param $uri An absolute URI. + * @param $params Parameters to be append as GET. + * + * @return + * An absolute URI with supplied parameters. + * + * @ingroup oauth2_section_4 + */ + private function buildUri($uri, $params) + { + $parse_url = parse_url($uri); + + // Add our params to the parsed uri + foreach ($params as $k => $v) { + if (isset($parse_url[$k])) { + $parse_url[$k] .= "&" . http_build_query($v, '', '&'); + } else { + $parse_url[$k] = http_build_query($v, '', '&'); + } + } + + // Put humpty dumpty back together + return + ((isset($parse_url["scheme"])) ? $parse_url["scheme"] . "://" : "") + . ((isset($parse_url["user"])) ? $parse_url["user"] + . ((isset($parse_url["pass"])) ? ":" . $parse_url["pass"] : "") . "@" : "") + . ((isset($parse_url["host"])) ? $parse_url["host"] : "") + . ((isset($parse_url["port"])) ? ":" . $parse_url["port"] : "") + . ((isset($parse_url["path"])) ? $parse_url["path"] : "") + . ((isset($parse_url["query"]) && !empty($parse_url['query'])) ? "?" . $parse_url["query"] : "") + . ((isset($parse_url["fragment"])) ? "#" . $parse_url["fragment"] : "") + ; + } + + protected function getValidResponseTypes() + { + return array( + self::RESPONSE_TYPE_ACCESS_TOKEN, + self::RESPONSE_TYPE_AUTHORIZATION_CODE, + ); + } + + /** + * Internal method for validating redirect URI supplied + * + * @param string $inputUri The submitted URI to be validated + * @param string $registeredUriString The allowed URI(s) to validate against. Can be a space-delimited string of URIs to + * allow for multiple URIs + * + * @see http://tools.ietf.org/html/rfc6749#section-3.1.2 + */ + protected function validateRedirectUri($inputUri, $registeredUriString) + { + if (!$inputUri || !$registeredUriString) { + return false; // if either one is missing, assume INVALID + } + + $registered_uris = preg_split('/\s+/', $registeredUriString); + foreach ($registered_uris as $registered_uri) { + if ($this->config['require_exact_redirect_uri']) { + // the input uri is validated against the registered uri using exact match + if (strcmp($inputUri, $registered_uri) === 0) { + return true; + } + } else { + $registered_uri_length = strlen($registered_uri); + if ($registered_uri_length === 0) { + return false; + } + + // the input uri is validated against the registered uri using case-insensitive match of the initial string + // i.e. additional query parameters may be applied + if (strcasecmp(substr($inputUri, 0, $registered_uri_length), $registered_uri) === 0) { + return true; + } + } + } + + return false; + } + + /** + * Convenience methods to access the parameters derived from the validated request + */ + + public function getScope() + { + return $this->scope; + } + + public function getState() + { + return $this->state; + } + + public function getClientId() + { + return $this->client_id; + } + + public function getRedirectUri() + { + return $this->redirect_uri; + } + + public function getResponseType() + { + return $this->response_type; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeControllerInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeControllerInterface.php new file mode 100644 index 000000000..fa07ae8d2 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeControllerInterface.php @@ -0,0 +1,43 @@ +<?php + +namespace OAuth2\Controller; + +use OAuth2\RequestInterface; +use OAuth2\ResponseInterface; + +/** + * This controller is called when a user should be authorized + * by an authorization server. As OAuth2 does not handle + * authorization directly, this controller ensures the request is valid, but + * requires the application to determine the value of $is_authorized + * + * ex: + * > $user_id = $this->somehowDetermineUserId(); + * > $is_authorized = $this->somehowDetermineUserAuthorization(); + * > $response = new OAuth2\Response(); + * > $authorizeController->handleAuthorizeRequest( + * > OAuth2\Request::createFromGlobals(), + * > $response, + * > $is_authorized, + * > $user_id); + * > $response->send(); + * + */ +interface AuthorizeControllerInterface +{ + /** + * List of possible authentication response types. + * The "authorization_code" mechanism exclusively supports 'code' + * and the "implicit" mechanism exclusively supports 'token'. + * + * @var string + * @see http://tools.ietf.org/html/rfc6749#section-4.1.1 + * @see http://tools.ietf.org/html/rfc6749#section-4.2.1 + */ + const RESPONSE_TYPE_AUTHORIZATION_CODE = 'code'; + const RESPONSE_TYPE_ACCESS_TOKEN = 'token'; + + public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null); + + public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceController.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceController.php new file mode 100644 index 000000000..3cfaaaf12 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceController.php @@ -0,0 +1,111 @@ +<?php + +namespace OAuth2\Controller; + +use OAuth2\TokenType\TokenTypeInterface; +use OAuth2\Storage\AccessTokenInterface; +use OAuth2\ScopeInterface; +use OAuth2\RequestInterface; +use OAuth2\ResponseInterface; +use OAuth2\Scope; + +/** + * @see OAuth2\Controller\ResourceControllerInterface + */ +class ResourceController implements ResourceControllerInterface +{ + private $token; + + protected $tokenType; + protected $tokenStorage; + protected $config; + protected $scopeUtil; + + public function __construct(TokenTypeInterface $tokenType, AccessTokenInterface $tokenStorage, $config = array(), ScopeInterface $scopeUtil = null) + { + $this->tokenType = $tokenType; + $this->tokenStorage = $tokenStorage; + + $this->config = array_merge(array( + 'www_realm' => 'Service', + ), $config); + + if (is_null($scopeUtil)) { + $scopeUtil = new Scope(); + } + $this->scopeUtil = $scopeUtil; + } + + public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null) + { + $token = $this->getAccessTokenData($request, $response); + + // Check if we have token data + if (is_null($token)) { + return false; + } + + /** + * Check scope, if provided + * If token doesn't have a scope, it's null/empty, or it's insufficient, then throw 403 + * @see http://tools.ietf.org/html/rfc6750#section-3.1 + */ + if ($scope && (!isset($token["scope"]) || !$token["scope"] || !$this->scopeUtil->checkScope($scope, $token["scope"]))) { + $response->setError(403, 'insufficient_scope', 'The request requires higher privileges than provided by the access token'); + $response->addHttpHeaders(array( + 'WWW-Authenticate' => sprintf('%s realm="%s", scope="%s", error="%s", error_description="%s"', + $this->tokenType->getTokenType(), + $this->config['www_realm'], + $scope, + $response->getParameter('error'), + $response->getParameter('error_description') + ) + )); + + return false; + } + + // allow retrieval of the token + $this->token = $token; + + return (bool) $token; + } + + public function getAccessTokenData(RequestInterface $request, ResponseInterface $response) + { + // Get the token parameter + if ($token_param = $this->tokenType->getAccessTokenParameter($request, $response)) { + // Get the stored token data (from the implementing subclass) + // Check we have a well formed token + // Check token expiration (expires is a mandatory paramter) + if (!$token = $this->tokenStorage->getAccessToken($token_param)) { + $response->setError(401, 'invalid_token', 'The access token provided is invalid'); + } elseif (!isset($token["expires"]) || !isset($token["client_id"])) { + $response->setError(401, 'malformed_token', 'Malformed token (missing "expires")'); + } elseif (time() > $token["expires"]) { + $response->setError(401, 'invalid_token', 'The access token provided has expired'); + } else { + return $token; + } + } + + $authHeader = sprintf('%s realm="%s"', $this->tokenType->getTokenType(), $this->config['www_realm']); + + if ($error = $response->getParameter('error')) { + $authHeader = sprintf('%s, error="%s"', $authHeader, $error); + if ($error_description = $response->getParameter('error_description')) { + $authHeader = sprintf('%s, error_description="%s"', $authHeader, $error_description); + } + } + + $response->addHttpHeaders(array('WWW-Authenticate' => $authHeader)); + + return null; + } + + // convenience method to allow retrieval of the token + public function getToken() + { + return $this->token; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceControllerInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceControllerInterface.php new file mode 100644 index 000000000..611421935 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceControllerInterface.php @@ -0,0 +1,26 @@ +<?php + +namespace OAuth2\Controller; + +use OAuth2\RequestInterface; +use OAuth2\ResponseInterface; + +/** + * This controller is called when a "resource" is requested. + * call verifyResourceRequest in order to determine if the request + * contains a valid token. + * + * ex: + * > if (!$resourceController->verifyResourceRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response())) { + * > $response->send(); // authorization failed + * > die(); + * > } + * > return json_encode($resource); // valid token! Send the stuff! + * + */ +interface ResourceControllerInterface +{ + public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null); + + public function getAccessTokenData(RequestInterface $request, ResponseInterface $response); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenController.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenController.php new file mode 100644 index 000000000..5d2d731fe --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenController.php @@ -0,0 +1,295 @@ +<?php + +namespace OAuth2\Controller; + +use OAuth2\ResponseType\AccessTokenInterface; +use OAuth2\ClientAssertionType\ClientAssertionTypeInterface; +use OAuth2\GrantType\GrantTypeInterface; +use OAuth2\ScopeInterface; +use OAuth2\Scope; +use OAuth2\Storage\ClientInterface; +use OAuth2\RequestInterface; +use OAuth2\ResponseInterface; + +/** + * @see \OAuth2\Controller\TokenControllerInterface + */ +class TokenController implements TokenControllerInterface +{ + /** + * @var AccessTokenInterface + */ + protected $accessToken; + + /** + * @var array + */ + protected $grantTypes; + + /** + * @var ClientAssertionTypeInterface + */ + protected $clientAssertionType; + + /** + * @var Scope|ScopeInterface + */ + protected $scopeUtil; + + /** + * @var ClientInterface + */ + protected $clientStorage; + + public function __construct(AccessTokenInterface $accessToken, ClientInterface $clientStorage, array $grantTypes = array(), ClientAssertionTypeInterface $clientAssertionType = null, ScopeInterface $scopeUtil = null) + { + if (is_null($clientAssertionType)) { + foreach ($grantTypes as $grantType) { + if (!$grantType instanceof ClientAssertionTypeInterface) { + throw new \InvalidArgumentException('You must supply an instance of OAuth2\ClientAssertionType\ClientAssertionTypeInterface or only use grant types which implement OAuth2\ClientAssertionType\ClientAssertionTypeInterface'); + } + } + } + $this->clientAssertionType = $clientAssertionType; + $this->accessToken = $accessToken; + $this->clientStorage = $clientStorage; + foreach ($grantTypes as $grantType) { + $this->addGrantType($grantType); + } + + if (is_null($scopeUtil)) { + $scopeUtil = new Scope(); + } + $this->scopeUtil = $scopeUtil; + } + + public function handleTokenRequest(RequestInterface $request, ResponseInterface $response) + { + if ($token = $this->grantAccessToken($request, $response)) { + // @see http://tools.ietf.org/html/rfc6749#section-5.1 + // server MUST disable caching in headers when tokens are involved + $response->setStatusCode(200); + $response->addParameters($token); + $response->addHttpHeaders(array( + 'Cache-Control' => 'no-store', + 'Pragma' => 'no-cache', + 'Content-Type' => 'application/json' + )); + } + } + + /** + * Grant or deny a requested access token. + * This would be called from the "/token" endpoint as defined in the spec. + * You can call your endpoint whatever you want. + * + * @param RequestInterface $request Request object to grant access token + * @param ResponseInterface $response + * + * @throws \InvalidArgumentException + * @throws \LogicException + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @see http://tools.ietf.org/html/rfc6749#section-10.6 + * @see http://tools.ietf.org/html/rfc6749#section-4.1.3 + * + * @ingroup oauth2_section_4 + */ + public function grantAccessToken(RequestInterface $request, ResponseInterface $response) + { + if (strtolower($request->server('REQUEST_METHOD')) != 'post') { + $response->setError(405, 'invalid_request', 'The request method must be POST when requesting an access token', '#section-3.2'); + $response->addHttpHeaders(array('Allow' => 'POST')); + + return null; + } + + /** + * Determine grant type from request + * and validate the request for that grant type + */ + if (!$grantTypeIdentifier = $request->request('grant_type')) { + $response->setError(400, 'invalid_request', 'The grant type was not specified in the request'); + + return null; + } + + if (!isset($this->grantTypes[$grantTypeIdentifier])) { + /* TODO: If this is an OAuth2 supported grant type that we have chosen not to implement, throw a 501 Not Implemented instead */ + $response->setError(400, 'unsupported_grant_type', sprintf('Grant type "%s" not supported', $grantTypeIdentifier)); + + return null; + } + + $grantType = $this->grantTypes[$grantTypeIdentifier]; + + /** + * Retrieve the client information from the request + * ClientAssertionTypes allow for grant types which also assert the client data + * in which case ClientAssertion is handled in the validateRequest method + * + * @see OAuth2\GrantType\JWTBearer + * @see OAuth2\GrantType\ClientCredentials + */ + if (!$grantType instanceof ClientAssertionTypeInterface) { + if (!$this->clientAssertionType->validateRequest($request, $response)) { + return null; + } + $clientId = $this->clientAssertionType->getClientId(); + } + + /** + * Retrieve the grant type information from the request + * The GrantTypeInterface object handles all validation + * If the object is an instance of ClientAssertionTypeInterface, + * That logic is handled here as well + */ + if (!$grantType->validateRequest($request, $response)) { + return null; + } + + if ($grantType instanceof ClientAssertionTypeInterface) { + $clientId = $grantType->getClientId(); + } else { + // validate the Client ID (if applicable) + if (!is_null($storedClientId = $grantType->getClientId()) && $storedClientId != $clientId) { + $response->setError(400, 'invalid_grant', sprintf('%s doesn\'t exist or is invalid for the client', $grantTypeIdentifier)); + + return null; + } + } + + /** + * Validate the client can use the requested grant type + */ + if (!$this->clientStorage->checkRestrictedGrantType($clientId, $grantTypeIdentifier)) { + $response->setError(400, 'unauthorized_client', 'The grant type is unauthorized for this client_id'); + + return false; + } + + /** + * Validate the scope of the token + * + * requestedScope - the scope specified in the token request + * availableScope - the scope associated with the grant type + * ex: in the case of the "Authorization Code" grant type, + * the scope is specified in the authorize request + * + * @see http://tools.ietf.org/html/rfc6749#section-3.3 + */ + + $requestedScope = $this->scopeUtil->getScopeFromRequest($request); + $availableScope = $grantType->getScope(); + + if ($requestedScope) { + // validate the requested scope + if ($availableScope) { + if (!$this->scopeUtil->checkScope($requestedScope, $availableScope)) { + $response->setError(400, 'invalid_scope', 'The scope requested is invalid for this request'); + + return null; + } + } else { + // validate the client has access to this scope + if ($clientScope = $this->clientStorage->getClientScope($clientId)) { + if (!$this->scopeUtil->checkScope($requestedScope, $clientScope)) { + $response->setError(400, 'invalid_scope', 'The scope requested is invalid for this client'); + + return false; + } + } elseif (!$this->scopeUtil->scopeExists($requestedScope)) { + $response->setError(400, 'invalid_scope', 'An unsupported scope was requested'); + + return null; + } + } + } elseif ($availableScope) { + // use the scope associated with this grant type + $requestedScope = $availableScope; + } else { + // use a globally-defined default scope + $defaultScope = $this->scopeUtil->getDefaultScope($clientId); + + // "false" means default scopes are not allowed + if (false === $defaultScope) { + $response->setError(400, 'invalid_scope', 'This application requires you specify a scope parameter'); + + return null; + } + + $requestedScope = $defaultScope; + } + + return $grantType->createAccessToken($this->accessToken, $clientId, $grantType->getUserId(), $requestedScope); + } + + /** + * addGrantType + * + * @param GrantTypeInterface $grantType the grant type to add for the specified identifier + * @param string $identifier a string passed in as "grant_type" in the response that will call this grantType + */ + public function addGrantType(GrantTypeInterface $grantType, $identifier = null) + { + if (is_null($identifier) || is_numeric($identifier)) { + $identifier = $grantType->getQuerystringIdentifier(); + } + + $this->grantTypes[$identifier] = $grantType; + } + + public function handleRevokeRequest(RequestInterface $request, ResponseInterface $response) + { + if ($this->revokeToken($request, $response)) { + $response->setStatusCode(200); + $response->addParameters(array('revoked' => true)); + } + } + + /** + * Revoke a refresh or access token. Returns true on success and when tokens are invalid + * + * Note: invalid tokens do not cause an error response since the client + * cannot handle such an error in a reasonable way. Moreover, the + * purpose of the revocation request, invalidating the particular token, + * is already achieved. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool|null + */ + public function revokeToken(RequestInterface $request, ResponseInterface $response) + { + if (strtolower($request->server('REQUEST_METHOD')) != 'post') { + $response->setError(405, 'invalid_request', 'The request method must be POST when revoking an access token', '#section-3.2'); + $response->addHttpHeaders(array('Allow' => 'POST')); + + return null; + } + + $token_type_hint = $request->request('token_type_hint'); + if (!in_array($token_type_hint, array(null, 'access_token', 'refresh_token'), true)) { + $response->setError(400, 'invalid_request', 'Token type hint must be either \'access_token\' or \'refresh_token\''); + + return null; + } + + $token = $request->request('token'); + if ($token === null) { + $response->setError(400, 'invalid_request', 'Missing token parameter to revoke'); + + return null; + } + + // @todo remove this check for v2.0 + if (!method_exists($this->accessToken, 'revokeToken')) { + $class = get_class($this->accessToken); + throw new \RuntimeException("AccessToken {$class} does not implement required revokeToken method"); + } + + $this->accessToken->revokeToken($token, $token_type_hint); + + return true; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenControllerInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenControllerInterface.php new file mode 100644 index 000000000..72d72570f --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenControllerInterface.php @@ -0,0 +1,32 @@ +<?php + +namespace OAuth2\Controller; + +use OAuth2\RequestInterface; +use OAuth2\ResponseInterface; + +/** + * This controller is called when a token is being requested. + * it is called to handle all grant types the application supports. + * It also validates the client's credentials + * + * ex: + * > $tokenController->handleTokenRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response()); + * > $response->send(); + * + */ +interface TokenControllerInterface +{ + /** + * handleTokenRequest + * + * @param $request + * OAuth2\RequestInterface - The current http request + * @param $response + * OAuth2\ResponseInterface - An instance of OAuth2\ResponseInterface to contain the response data + * + */ + public function handleTokenRequest(RequestInterface $request, ResponseInterface $response); + + public function grantAccessToken(RequestInterface $request, ResponseInterface $response); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/EncryptionInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/EncryptionInterface.php new file mode 100644 index 000000000..2d336c664 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/EncryptionInterface.php @@ -0,0 +1,11 @@ +<?php + +namespace OAuth2\Encryption; + +interface EncryptionInterface +{ + public function encode($payload, $key, $algorithm = null); + public function decode($payload, $key, $algorithm = null); + public function urlSafeB64Encode($data); + public function urlSafeB64Decode($b64); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/FirebaseJwt.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/FirebaseJwt.php new file mode 100644 index 000000000..1b527e0a0 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/FirebaseJwt.php @@ -0,0 +1,47 @@ +<?php + +namespace OAuth2\Encryption; + +/** + * Bridge file to use the firebase/php-jwt package for JWT encoding and decoding. + * @author Francis Chuang <francis.chuang@gmail.com> + */ +class FirebaseJwt implements EncryptionInterface +{ + public function __construct() + { + if (!class_exists('\JWT')) { + throw new \ErrorException('firebase/php-jwt must be installed to use this feature. You can do this by running "composer require firebase/php-jwt"'); + } + } + + public function encode($payload, $key, $alg = 'HS256', $keyId = null) + { + return \JWT::encode($payload, $key, $alg, $keyId); + } + + public function decode($jwt, $key = null, $allowedAlgorithms = null) + { + try { + + //Maintain BC: Do not verify if no algorithms are passed in. + if (!$allowedAlgorithms) { + $key = null; + } + + return (array)\JWT::decode($jwt, $key, $allowedAlgorithms); + } catch (\Exception $e) { + return false; + } + } + + public function urlSafeB64Encode($data) + { + return \JWT::urlsafeB64Encode($data); + } + + public function urlSafeB64Decode($b64) + { + return \JWT::urlsafeB64Decode($b64); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/Jwt.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/Jwt.php new file mode 100644 index 000000000..ee576e643 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/Jwt.php @@ -0,0 +1,173 @@ +<?php + +namespace OAuth2\Encryption; + +/** + * @link https://github.com/F21/jwt + * @author F21 + */ +class Jwt implements EncryptionInterface +{ + public function encode($payload, $key, $algo = 'HS256') + { + $header = $this->generateJwtHeader($payload, $algo); + + $segments = array( + $this->urlSafeB64Encode(json_encode($header)), + $this->urlSafeB64Encode(json_encode($payload)) + ); + + $signing_input = implode('.', $segments); + + $signature = $this->sign($signing_input, $key, $algo); + $segments[] = $this->urlsafeB64Encode($signature); + + return implode('.', $segments); + } + + public function decode($jwt, $key = null, $allowedAlgorithms = true) + { + if (!strpos($jwt, '.')) { + return false; + } + + $tks = explode('.', $jwt); + + if (count($tks) != 3) { + return false; + } + + list($headb64, $payloadb64, $cryptob64) = $tks; + + if (null === ($header = json_decode($this->urlSafeB64Decode($headb64), true))) { + return false; + } + + if (null === $payload = json_decode($this->urlSafeB64Decode($payloadb64), true)) { + return false; + } + + $sig = $this->urlSafeB64Decode($cryptob64); + + if ((bool) $allowedAlgorithms) { + if (!isset($header['alg'])) { + return false; + } + + // check if bool arg supplied here to maintain BC + if (is_array($allowedAlgorithms) && !in_array($header['alg'], $allowedAlgorithms)) { + return false; + } + + if (!$this->verifySignature($sig, "$headb64.$payloadb64", $key, $header['alg'])) { + return false; + } + } + + return $payload; + } + + private function verifySignature($signature, $input, $key, $algo = 'HS256') + { + // use constants when possible, for HipHop support + switch ($algo) { + case'HS256': + case'HS384': + case'HS512': + return $this->hash_equals( + $this->sign($input, $key, $algo), + $signature + ); + + case 'RS256': + return openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256') === 1; + + case 'RS384': + return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'sha384') === 1; + + case 'RS512': + return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512') === 1; + + default: + throw new \InvalidArgumentException("Unsupported or invalid signing algorithm."); + } + } + + private function sign($input, $key, $algo = 'HS256') + { + switch ($algo) { + case 'HS256': + return hash_hmac('sha256', $input, $key, true); + + case 'HS384': + return hash_hmac('sha384', $input, $key, true); + + case 'HS512': + return hash_hmac('sha512', $input, $key, true); + + case 'RS256': + return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256'); + + case 'RS384': + return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'sha384'); + + case 'RS512': + return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512'); + + default: + throw new \Exception("Unsupported or invalid signing algorithm."); + } + } + + private function generateRSASignature($input, $key, $algo) + { + if (!openssl_sign($input, $signature, $key, $algo)) { + throw new \Exception("Unable to sign data."); + } + + return $signature; + } + + public function urlSafeB64Encode($data) + { + $b64 = base64_encode($data); + $b64 = str_replace(array('+', '/', "\r", "\n", '='), + array('-', '_'), + $b64); + + return $b64; + } + + public function urlSafeB64Decode($b64) + { + $b64 = str_replace(array('-', '_'), + array('+', '/'), + $b64); + + return base64_decode($b64); + } + + /** + * Override to create a custom header + */ + protected function generateJwtHeader($payload, $algorithm) + { + return array( + 'typ' => 'JWT', + 'alg' => $algorithm, + ); + } + + protected function hash_equals($a, $b) + { + if (function_exists('hash_equals')) { + return hash_equals($a, $b); + } + $diff = strlen($a) ^ strlen($b); + for ($i = 0; $i < strlen($a) && $i < strlen($b); $i++) { + $diff |= ord($a[$i]) ^ ord($b[$i]); + } + + return $diff === 0; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/AuthorizationCode.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/AuthorizationCode.php new file mode 100644 index 000000000..cae9f787d --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/AuthorizationCode.php @@ -0,0 +1,100 @@ +<?php + +namespace OAuth2\GrantType; + +use OAuth2\Storage\AuthorizationCodeInterface; +use OAuth2\ResponseType\AccessTokenInterface; +use OAuth2\RequestInterface; +use OAuth2\ResponseInterface; + +/** + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +class AuthorizationCode implements GrantTypeInterface +{ + protected $storage; + protected $authCode; + + /** + * @param \OAuth2\Storage\AuthorizationCodeInterface $storage REQUIRED Storage class for retrieving authorization code information + */ + public function __construct(AuthorizationCodeInterface $storage) + { + $this->storage = $storage; + } + + public function getQuerystringIdentifier() + { + return 'authorization_code'; + } + + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$request->request('code')) { + $response->setError(400, 'invalid_request', 'Missing parameter: "code" is required'); + + return false; + } + + $code = $request->request('code'); + if (!$authCode = $this->storage->getAuthorizationCode($code)) { + $response->setError(400, 'invalid_grant', 'Authorization code doesn\'t exist or is invalid for the client'); + + return false; + } + + /* + * 4.1.3 - ensure that the "redirect_uri" parameter is present if the "redirect_uri" parameter was included in the initial authorization request + * @uri - http://tools.ietf.org/html/rfc6749#section-4.1.3 + */ + if (isset($authCode['redirect_uri']) && $authCode['redirect_uri']) { + if (!$request->request('redirect_uri') || urldecode($request->request('redirect_uri')) != urldecode($authCode['redirect_uri'])) { + $response->setError(400, 'redirect_uri_mismatch', "The redirect URI is missing or do not match", "#section-4.1.3"); + + return false; + } + } + + if (!isset($authCode['expires'])) { + throw new \Exception('Storage must return authcode with a value for "expires"'); + } + + if ($authCode["expires"] < time()) { + $response->setError(400, 'invalid_grant', "The authorization code has expired"); + + return false; + } + + if (!isset($authCode['code'])) { + $authCode['code'] = $code; // used to expire the code after the access token is granted + } + + $this->authCode = $authCode; + + return true; + } + + public function getClientId() + { + return $this->authCode['client_id']; + } + + public function getScope() + { + return isset($this->authCode['scope']) ? $this->authCode['scope'] : null; + } + + public function getUserId() + { + return isset($this->authCode['user_id']) ? $this->authCode['user_id'] : null; + } + + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + $token = $accessToken->createAccessToken($client_id, $user_id, $scope); + $this->storage->expireAuthorizationCode($this->authCode['code']); + + return $token; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/ClientCredentials.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/ClientCredentials.php new file mode 100644 index 000000000..f953e4e8d --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/ClientCredentials.php @@ -0,0 +1,67 @@ +<?php + +namespace OAuth2\GrantType; + +use OAuth2\ClientAssertionType\HttpBasic; +use OAuth2\ResponseType\AccessTokenInterface; +use OAuth2\Storage\ClientCredentialsInterface; + +/** + * @author Brent Shaffer <bshafs at gmail dot com> + * + * @see OAuth2\ClientAssertionType_HttpBasic + */ +class ClientCredentials extends HttpBasic implements GrantTypeInterface +{ + private $clientData; + + public function __construct(ClientCredentialsInterface $storage, array $config = array()) + { + /** + * The client credentials grant type MUST only be used by confidential clients + * + * @see http://tools.ietf.org/html/rfc6749#section-4.4 + */ + $config['allow_public_clients'] = false; + + parent::__construct($storage, $config); + } + + public function getQuerystringIdentifier() + { + return 'client_credentials'; + } + + public function getScope() + { + $this->loadClientData(); + + return isset($this->clientData['scope']) ? $this->clientData['scope'] : null; + } + + public function getUserId() + { + $this->loadClientData(); + + return isset($this->clientData['user_id']) ? $this->clientData['user_id'] : null; + } + + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + /** + * Client Credentials Grant does NOT include a refresh token + * + * @see http://tools.ietf.org/html/rfc6749#section-4.4.3 + */ + $includeRefreshToken = false; + + return $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken); + } + + private function loadClientData() + { + if (!$this->clientData) { + $this->clientData = $this->storage->getClientDetails($this->getClientId()); + } + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/GrantTypeInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/GrantTypeInterface.php new file mode 100644 index 000000000..98489e9c1 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/GrantTypeInterface.php @@ -0,0 +1,20 @@ +<?php + +namespace OAuth2\GrantType; + +use OAuth2\ResponseType\AccessTokenInterface; +use OAuth2\RequestInterface; +use OAuth2\ResponseInterface; + +/** + * Interface for all OAuth2 Grant Types + */ +interface GrantTypeInterface +{ + public function getQuerystringIdentifier(); + public function validateRequest(RequestInterface $request, ResponseInterface $response); + public function getClientId(); + public function getUserId(); + public function getScope(); + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/JwtBearer.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/JwtBearer.php new file mode 100644 index 000000000..bb11a6954 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/JwtBearer.php @@ -0,0 +1,226 @@ +<?php + +namespace OAuth2\GrantType; + +use OAuth2\ClientAssertionType\ClientAssertionTypeInterface; +use OAuth2\Storage\JwtBearerInterface; +use OAuth2\Encryption\Jwt; +use OAuth2\Encryption\EncryptionInterface; +use OAuth2\ResponseType\AccessTokenInterface; +use OAuth2\RequestInterface; +use OAuth2\ResponseInterface; + +/** + * The JWT bearer authorization grant implements JWT (JSON Web Tokens) as a grant type per the IETF draft. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-04#section-4 + * + * @author F21 + * @author Brent Shaffer <bshafs at gmail dot com> + */ +class JwtBearer implements GrantTypeInterface, ClientAssertionTypeInterface +{ + private $jwt; + + protected $storage; + protected $audience; + protected $jwtUtil; + protected $allowedAlgorithms; + + /** + * Creates an instance of the JWT bearer grant type. + * + * @param OAuth2\Storage\JWTBearerInterface|JwtBearerInterface $storage A valid storage interface that implements storage hooks for the JWT bearer grant type. + * @param string $audience The audience to validate the token against. This is usually the full URI of the OAuth token requests endpoint. + * @param EncryptionInterface|OAuth2\Encryption\JWT $jwtUtil OPTONAL The class used to decode, encode and verify JWTs. + * @param array $config + */ + public function __construct(JwtBearerInterface $storage, $audience, EncryptionInterface $jwtUtil = null, array $config = array()) + { + $this->storage = $storage; + $this->audience = $audience; + + if (is_null($jwtUtil)) { + $jwtUtil = new Jwt(); + } + + $this->config = array_merge(array( + 'allowed_algorithms' => array('RS256', 'RS384', 'RS512') + ), $config); + + $this->jwtUtil = $jwtUtil; + + $this->allowedAlgorithms = $this->config['allowed_algorithms']; + } + + /** + * Returns the grant_type get parameter to identify the grant type request as JWT bearer authorization grant. + * + * @return + * The string identifier for grant_type. + * + * @see OAuth2\GrantType\GrantTypeInterface::getQuerystringIdentifier() + */ + public function getQuerystringIdentifier() + { + return 'urn:ietf:params:oauth:grant-type:jwt-bearer'; + } + + /** + * Validates the data from the decoded JWT. + * + * @return + * TRUE if the JWT request is valid and can be decoded. Otherwise, FALSE is returned. + * + * @see OAuth2\GrantType\GrantTypeInterface::getTokenData() + */ + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$request->request("assertion")) { + $response->setError(400, 'invalid_request', 'Missing parameters: "assertion" required'); + + return null; + } + + // Store the undecoded JWT for later use + $undecodedJWT = $request->request('assertion'); + + // Decode the JWT + $jwt = $this->jwtUtil->decode($request->request('assertion'), null, false); + + if (!$jwt) { + $response->setError(400, 'invalid_request', "JWT is malformed"); + + return null; + } + + // ensure these properties contain a value + // @todo: throw malformed error for missing properties + $jwt = array_merge(array( + 'scope' => null, + 'iss' => null, + 'sub' => null, + 'aud' => null, + 'exp' => null, + 'nbf' => null, + 'iat' => null, + 'jti' => null, + 'typ' => null, + ), $jwt); + + if (!isset($jwt['iss'])) { + $response->setError(400, 'invalid_grant', "Invalid issuer (iss) provided"); + + return null; + } + + if (!isset($jwt['sub'])) { + $response->setError(400, 'invalid_grant', "Invalid subject (sub) provided"); + + return null; + } + + if (!isset($jwt['exp'])) { + $response->setError(400, 'invalid_grant', "Expiration (exp) time must be present"); + + return null; + } + + // Check expiration + if (ctype_digit($jwt['exp'])) { + if ($jwt['exp'] <= time()) { + $response->setError(400, 'invalid_grant', "JWT has expired"); + + return null; + } + } else { + $response->setError(400, 'invalid_grant', "Expiration (exp) time must be a unix time stamp"); + + return null; + } + + // Check the not before time + if ($notBefore = $jwt['nbf']) { + if (ctype_digit($notBefore)) { + if ($notBefore > time()) { + $response->setError(400, 'invalid_grant', "JWT cannot be used before the Not Before (nbf) time"); + + return null; + } + } else { + $response->setError(400, 'invalid_grant', "Not Before (nbf) time must be a unix time stamp"); + + return null; + } + } + + // Check the audience if required to match + if (!isset($jwt['aud']) || ($jwt['aud'] != $this->audience)) { + $response->setError(400, 'invalid_grant', "Invalid audience (aud)"); + + return null; + } + + // Check the jti (nonce) + // @see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-13#section-4.1.7 + if (isset($jwt['jti'])) { + $jti = $this->storage->getJti($jwt['iss'], $jwt['sub'], $jwt['aud'], $jwt['exp'], $jwt['jti']); + + //Reject if jti is used and jwt is still valid (exp parameter has not expired). + if ($jti && $jti['expires'] > time()) { + $response->setError(400, 'invalid_grant', "JSON Token Identifier (jti) has already been used"); + + return null; + } else { + $this->storage->setJti($jwt['iss'], $jwt['sub'], $jwt['aud'], $jwt['exp'], $jwt['jti']); + } + } + + // Get the iss's public key + // @see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06#section-4.1.1 + if (!$key = $this->storage->getClientKey($jwt['iss'], $jwt['sub'])) { + $response->setError(400, 'invalid_grant', "Invalid issuer (iss) or subject (sub) provided"); + + return null; + } + + // Verify the JWT + if (!$this->jwtUtil->decode($undecodedJWT, $key, $this->allowedAlgorithms)) { + $response->setError(400, 'invalid_grant', "JWT failed signature verification"); + + return null; + } + + $this->jwt = $jwt; + + return true; + } + + public function getClientId() + { + return $this->jwt['iss']; + } + + public function getUserId() + { + return $this->jwt['sub']; + } + + public function getScope() + { + return null; + } + + /** + * Creates an access token that is NOT associated with a refresh token. + * If a subject (sub) the name of the user/account we are accessing data on behalf of. + * + * @see OAuth2\GrantType\GrantTypeInterface::createAccessToken() + */ + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + $includeRefreshToken = false; + + return $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/RefreshToken.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/RefreshToken.php new file mode 100644 index 000000000..e55385222 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/RefreshToken.php @@ -0,0 +1,111 @@ +<?php + +namespace OAuth2\GrantType; + +use OAuth2\Storage\RefreshTokenInterface; +use OAuth2\ResponseType\AccessTokenInterface; +use OAuth2\RequestInterface; +use OAuth2\ResponseInterface; + +/** + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +class RefreshToken implements GrantTypeInterface +{ + private $refreshToken; + + protected $storage; + protected $config; + + /** + * @param OAuth2\Storage\RefreshTokenInterface $storage REQUIRED Storage class for retrieving refresh token information + * @param array $config OPTIONAL Configuration options for the server + * <code> + * $config = array( + * 'always_issue_new_refresh_token' => true, // whether to issue a new refresh token upon successful token request + * 'unset_refresh_token_after_use' => true // whether to unset the refresh token after after using + * ); + * </code> + */ + public function __construct(RefreshTokenInterface $storage, $config = array()) + { + $this->config = array_merge(array( + 'always_issue_new_refresh_token' => false, + 'unset_refresh_token_after_use' => true + ), $config); + + // to preserve B.C. with v1.6 + // @see https://github.com/bshaffer/oauth2-server-php/pull/580 + // @todo - remove in v2.0 + if (isset($config['always_issue_new_refresh_token']) && !isset($config['unset_refresh_token_after_use'])) { + $this->config['unset_refresh_token_after_use'] = $config['always_issue_new_refresh_token']; + } + + $this->storage = $storage; + } + + public function getQuerystringIdentifier() + { + return 'refresh_token'; + } + + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$request->request("refresh_token")) { + $response->setError(400, 'invalid_request', 'Missing parameter: "refresh_token" is required'); + + return null; + } + + if (!$refreshToken = $this->storage->getRefreshToken($request->request("refresh_token"))) { + $response->setError(400, 'invalid_grant', 'Invalid refresh token'); + + return null; + } + + if ($refreshToken['expires'] > 0 && $refreshToken["expires"] < time()) { + $response->setError(400, 'invalid_grant', 'Refresh token has expired'); + + return null; + } + + // store the refresh token locally so we can delete it when a new refresh token is generated + $this->refreshToken = $refreshToken; + + return true; + } + + public function getClientId() + { + return $this->refreshToken['client_id']; + } + + public function getUserId() + { + return isset($this->refreshToken['user_id']) ? $this->refreshToken['user_id'] : null; + } + + public function getScope() + { + return isset($this->refreshToken['scope']) ? $this->refreshToken['scope'] : null; + } + + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + /* + * It is optional to force a new refresh token when a refresh token is used. + * However, if a new refresh token is issued, the old one MUST be expired + * @see http://tools.ietf.org/html/rfc6749#section-6 + */ + $issueNewRefreshToken = $this->config['always_issue_new_refresh_token']; + $unsetRefreshToken = $this->config['unset_refresh_token_after_use']; + $token = $accessToken->createAccessToken($client_id, $user_id, $scope, $issueNewRefreshToken); + + if ($unsetRefreshToken) { + $this->storage->unsetRefreshToken($this->refreshToken['refresh_token']); + } + + return $token; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/UserCredentials.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/UserCredentials.php new file mode 100644 index 000000000..f165538ba --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/UserCredentials.php @@ -0,0 +1,83 @@ +<?php + +namespace OAuth2\GrantType; + +use OAuth2\Storage\UserCredentialsInterface; +use OAuth2\ResponseType\AccessTokenInterface; +use OAuth2\RequestInterface; +use OAuth2\ResponseInterface; + +/** + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +class UserCredentials implements GrantTypeInterface +{ + private $userInfo; + + protected $storage; + + /** + * @param OAuth2\Storage\UserCredentialsInterface $storage REQUIRED Storage class for retrieving user credentials information + */ + public function __construct(UserCredentialsInterface $storage) + { + $this->storage = $storage; + } + + public function getQuerystringIdentifier() + { + return 'password'; + } + + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$request->request("password") || !$request->request("username")) { + $response->setError(400, 'invalid_request', 'Missing parameters: "username" and "password" required'); + + return null; + } + + if (!$this->storage->checkUserCredentials($request->request("username"), $request->request("password"))) { + $response->setError(401, 'invalid_grant', 'Invalid username and password combination'); + + return null; + } + + $userInfo = $this->storage->getUserDetails($request->request("username")); + + if (empty($userInfo)) { + $response->setError(400, 'invalid_grant', 'Unable to retrieve user information'); + + return null; + } + + if (!isset($userInfo['user_id'])) { + throw new \LogicException("you must set the user_id on the array returned by getUserDetails"); + } + + $this->userInfo = $userInfo; + + return true; + } + + public function getClientId() + { + return null; + } + + public function getUserId() + { + return $this->userInfo['user_id']; + } + + public function getScope() + { + return isset($this->userInfo['scope']) ? $this->userInfo['scope'] : null; + } + + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + return $accessToken->createAccessToken($client_id, $user_id, $scope); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeController.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeController.php new file mode 100644 index 000000000..c9b5c6af7 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeController.php @@ -0,0 +1,106 @@ +<?php + +namespace OAuth2\OpenID\Controller; + +use OAuth2\Controller\AuthorizeController as BaseAuthorizeController; +use OAuth2\RequestInterface; +use OAuth2\ResponseInterface; + +/** + * @see OAuth2\Controller\AuthorizeControllerInterface + */ +class AuthorizeController extends BaseAuthorizeController implements AuthorizeControllerInterface +{ + private $nonce; + + protected function setNotAuthorizedResponse(RequestInterface $request, ResponseInterface $response, $redirect_uri, $user_id = null) + { + $prompt = $request->query('prompt', 'consent'); + if ($prompt == 'none') { + if (is_null($user_id)) { + $error = 'login_required'; + $error_message = 'The user must log in'; + } else { + $error = 'interaction_required'; + $error_message = 'The user must grant access to your application'; + } + } else { + $error = 'consent_required'; + $error_message = 'The user denied access to your application'; + } + + $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $this->getState(), $error, $error_message); + } + + protected function buildAuthorizeParameters($request, $response, $user_id) + { + if (!$params = parent::buildAuthorizeParameters($request, $response, $user_id)) { + return; + } + + // Generate an id token if needed. + if ($this->needsIdToken($this->getScope()) && $this->getResponseType() == self::RESPONSE_TYPE_AUTHORIZATION_CODE) { + $params['id_token'] = $this->responseTypes['id_token']->createIdToken($this->getClientId(), $user_id, $this->nonce); + } + + // add the nonce to return with the redirect URI + $params['nonce'] = $this->nonce; + + return $params; + } + + public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response) + { + if (!parent::validateAuthorizeRequest($request, $response)) { + return false; + } + + $nonce = $request->query('nonce'); + + // Validate required nonce for "id_token" and "id_token token" + if (!$nonce && in_array($this->getResponseType(), array(self::RESPONSE_TYPE_ID_TOKEN, self::RESPONSE_TYPE_ID_TOKEN_TOKEN))) { + $response->setError(400, 'invalid_nonce', 'This application requires you specify a nonce parameter'); + + return false; + } + + $this->nonce = $nonce; + + return true; + } + + protected function getValidResponseTypes() + { + return array( + self::RESPONSE_TYPE_ACCESS_TOKEN, + self::RESPONSE_TYPE_AUTHORIZATION_CODE, + self::RESPONSE_TYPE_ID_TOKEN, + self::RESPONSE_TYPE_ID_TOKEN_TOKEN, + self::RESPONSE_TYPE_CODE_ID_TOKEN, + ); + } + + /** + * Returns whether the current request needs to generate an id token. + * + * ID Tokens are a part of the OpenID Connect specification, so this + * method checks whether OpenID Connect is enabled in the server settings + * and whether the openid scope was requested. + * + * @param $request_scope + * A space-separated string of scopes. + * + * @return + * TRUE if an id token is needed, FALSE otherwise. + */ + public function needsIdToken($request_scope) + { + // see if the "openid" scope exists in the requested scope + return $this->scopeUtil->checkScope('openid', $request_scope); + } + + public function getNonce() + { + return $this->nonce; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php new file mode 100644 index 000000000..1e231d844 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php @@ -0,0 +1,10 @@ +<?php + +namespace OAuth2\OpenID\Controller; + +interface AuthorizeControllerInterface +{ + const RESPONSE_TYPE_ID_TOKEN = 'id_token'; + const RESPONSE_TYPE_ID_TOKEN_TOKEN = 'id_token token'; + const RESPONSE_TYPE_CODE_ID_TOKEN = 'code id_token'; +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/UserInfoController.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/UserInfoController.php new file mode 100644 index 000000000..30cb942d0 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/UserInfoController.php @@ -0,0 +1,58 @@ +<?php + +namespace OAuth2\OpenID\Controller; + +use OAuth2\Scope; +use OAuth2\TokenType\TokenTypeInterface; +use OAuth2\Storage\AccessTokenInterface; +use OAuth2\OpenID\Storage\UserClaimsInterface; +use OAuth2\Controller\ResourceController; +use OAuth2\ScopeInterface; +use OAuth2\RequestInterface; +use OAuth2\ResponseInterface; + +/** + * @see OAuth2\Controller\UserInfoControllerInterface + */ +class UserInfoController extends ResourceController implements UserInfoControllerInterface +{ + private $token; + + protected $tokenType; + protected $tokenStorage; + protected $userClaimsStorage; + protected $config; + protected $scopeUtil; + + public function __construct(TokenTypeInterface $tokenType, AccessTokenInterface $tokenStorage, UserClaimsInterface $userClaimsStorage, $config = array(), ScopeInterface $scopeUtil = null) + { + $this->tokenType = $tokenType; + $this->tokenStorage = $tokenStorage; + $this->userClaimsStorage = $userClaimsStorage; + + $this->config = array_merge(array( + 'www_realm' => 'Service', + ), $config); + + if (is_null($scopeUtil)) { + $scopeUtil = new Scope(); + } + $this->scopeUtil = $scopeUtil; + } + + public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response) + { + if (!$this->verifyResourceRequest($request, $response, 'openid')) { + return; + } + + $token = $this->getToken(); + $claims = $this->userClaimsStorage->getUserClaims($token['user_id'], $token['scope']); + // The sub Claim MUST always be returned in the UserInfo Response. + // http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse + $claims += array( + 'sub' => $token['user_id'], + ); + $response->addParameters($claims); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php new file mode 100644 index 000000000..a89049d49 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php @@ -0,0 +1,23 @@ +<?php + +namespace OAuth2\OpenID\Controller; + +use OAuth2\RequestInterface; +use OAuth2\ResponseInterface; + +/** + * This controller is called when the user claims for OpenID Connect's + * UserInfo endpoint should be returned. + * + * ex: + * > $response = new OAuth2\Response(); + * > $userInfoController->handleUserInfoRequest( + * > OAuth2\Request::createFromGlobals(), + * > $response; + * > $response->send(); + * + */ +interface UserInfoControllerInterface +{ + public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/GrantType/AuthorizationCode.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/GrantType/AuthorizationCode.php new file mode 100644 index 000000000..8ed1edc26 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/GrantType/AuthorizationCode.php @@ -0,0 +1,33 @@ +<?php + +namespace OAuth2\OpenID\GrantType; + +use OAuth2\GrantType\AuthorizationCode as BaseAuthorizationCode; +use OAuth2\ResponseType\AccessTokenInterface; + +/** + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +class AuthorizationCode extends BaseAuthorizationCode +{ + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + $includeRefreshToken = true; + if (isset($this->authCode['id_token'])) { + // OpenID Connect requests include the refresh token only if the + // offline_access scope has been requested and granted. + $scopes = explode(' ', trim($scope)); + $includeRefreshToken = in_array('offline_access', $scopes); + } + + $token = $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken); + if (isset($this->authCode['id_token'])) { + $token['id_token'] = $this->authCode['id_token']; + } + + $this->storage->expireAuthorizationCode($this->authCode['code']); + + return $token; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php new file mode 100644 index 000000000..8971954c5 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php @@ -0,0 +1,60 @@ +<?php + +namespace OAuth2\OpenID\ResponseType; + +use OAuth2\ResponseType\AuthorizationCode as BaseAuthorizationCode; +use OAuth2\OpenID\Storage\AuthorizationCodeInterface as AuthorizationCodeStorageInterface; + +/** + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +class AuthorizationCode extends BaseAuthorizationCode implements AuthorizationCodeInterface +{ + public function __construct(AuthorizationCodeStorageInterface $storage, array $config = array()) + { + parent::__construct($storage, $config); + } + + public function getAuthorizeResponse($params, $user_id = null) + { + // build the URL to redirect to + $result = array('query' => array()); + + $params += array('scope' => null, 'state' => null, 'id_token' => null); + + $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope'], $params['id_token']); + + if (isset($params['state'])) { + $result['query']['state'] = $params['state']; + } + + return array($params['redirect_uri'], $result); + } + + /** + * Handle the creation of the authorization code. + * + * @param $client_id + * Client identifier related to the authorization code + * @param $user_id + * User ID associated with the authorization code + * @param $redirect_uri + * An absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * @param $id_token + * (optional) The OpenID Connect id_token. + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @ingroup oauth2_section_4 + */ + public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null) + { + $code = $this->generateAuthorizationCode(); + $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope, $id_token); + + return $code; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php new file mode 100644 index 000000000..ea4779255 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php @@ -0,0 +1,27 @@ +<?php + +namespace OAuth2\OpenID\ResponseType; + +use OAuth2\ResponseType\AuthorizationCodeInterface as BaseAuthorizationCodeInterface; + +/** + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +interface AuthorizationCodeInterface extends BaseAuthorizationCodeInterface +{ + /** + * Handle the creation of the authorization code. + * + * @param $client_id Client identifier related to the authorization code + * @param $user_id User ID associated with the authorization code + * @param $redirect_uri An absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param $scope OPTIONAL Scopes to be stored in space-separated string. + * @param $id_token OPTIONAL The OpenID Connect id_token. + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @ingroup oauth2_section_4 + */ + public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdToken.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdToken.php new file mode 100644 index 000000000..ac7764d6c --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdToken.php @@ -0,0 +1,24 @@ +<?php + +namespace OAuth2\OpenID\ResponseType; + +class CodeIdToken implements CodeIdTokenInterface +{ + protected $authCode; + protected $idToken; + + public function __construct(AuthorizationCodeInterface $authCode, IdTokenInterface $idToken) + { + $this->authCode = $authCode; + $this->idToken = $idToken; + } + + public function getAuthorizeResponse($params, $user_id = null) + { + $result = $this->authCode->getAuthorizeResponse($params, $user_id); + $resultIdToken = $this->idToken->getAuthorizeResponse($params, $user_id); + $result[1]['query']['id_token'] = $resultIdToken[1]['fragment']['id_token']; + + return $result; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php new file mode 100644 index 000000000..629adcca8 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php @@ -0,0 +1,9 @@ +<?php + +namespace OAuth2\OpenID\ResponseType; + +use OAuth2\ResponseType\ResponseTypeInterface; + +interface CodeIdTokenInterface extends ResponseTypeInterface +{ +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdToken.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdToken.php new file mode 100644 index 000000000..97777fbf2 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdToken.php @@ -0,0 +1,124 @@ +<?php + +namespace OAuth2\OpenID\ResponseType; + +use OAuth2\Encryption\EncryptionInterface; +use OAuth2\Encryption\Jwt; +use OAuth2\Storage\PublicKeyInterface; +use OAuth2\OpenID\Storage\UserClaimsInterface; + +class IdToken implements IdTokenInterface +{ + protected $userClaimsStorage; + protected $publicKeyStorage; + protected $config; + protected $encryptionUtil; + + public function __construct(UserClaimsInterface $userClaimsStorage, PublicKeyInterface $publicKeyStorage, array $config = array(), EncryptionInterface $encryptionUtil = null) + { + $this->userClaimsStorage = $userClaimsStorage; + $this->publicKeyStorage = $publicKeyStorage; + if (is_null($encryptionUtil)) { + $encryptionUtil = new Jwt(); + } + $this->encryptionUtil = $encryptionUtil; + + if (!isset($config['issuer'])) { + throw new \LogicException('config parameter "issuer" must be set'); + } + $this->config = array_merge(array( + 'id_lifetime' => 3600, + ), $config); + } + + public function getAuthorizeResponse($params, $userInfo = null) + { + // build the URL to redirect to + $result = array('query' => array()); + $params += array('scope' => null, 'state' => null, 'nonce' => null); + + // create the id token. + list($user_id, $auth_time) = $this->getUserIdAndAuthTime($userInfo); + $userClaims = $this->userClaimsStorage->getUserClaims($user_id, $params['scope']); + + $id_token = $this->createIdToken($params['client_id'], $userInfo, $params['nonce'], $userClaims, null); + $result["fragment"] = array('id_token' => $id_token); + if (isset($params['state'])) { + $result["fragment"]["state"] = $params['state']; + } + + return array($params['redirect_uri'], $result); + } + + public function createIdToken($client_id, $userInfo, $nonce = null, $userClaims = null, $access_token = null) + { + // pull auth_time from user info if supplied + list($user_id, $auth_time) = $this->getUserIdAndAuthTime($userInfo); + + $token = array( + 'iss' => $this->config['issuer'], + 'sub' => $user_id, + 'aud' => $client_id, + 'iat' => time(), + 'exp' => time() + $this->config['id_lifetime'], + 'auth_time' => $auth_time, + ); + + if ($nonce) { + $token['nonce'] = $nonce; + } + + if ($userClaims) { + $token += $userClaims; + } + + if ($access_token) { + $token['at_hash'] = $this->createAtHash($access_token, $client_id); + } + + return $this->encodeToken($token, $client_id); + } + + protected function createAtHash($access_token, $client_id = null) + { + // maps HS256 and RS256 to sha256, etc. + $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); + $hash_algorithm = 'sha' . substr($algorithm, 2); + $hash = hash($hash_algorithm, $access_token, true); + $at_hash = substr($hash, 0, strlen($hash) / 2); + + return $this->encryptionUtil->urlSafeB64Encode($at_hash); + } + + protected function encodeToken(array $token, $client_id = null) + { + $private_key = $this->publicKeyStorage->getPrivateKey($client_id); + $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); + + return $this->encryptionUtil->encode($token, $private_key, $algorithm); + } + + private function getUserIdAndAuthTime($userInfo) + { + $auth_time = null; + + // support an array for user_id / auth_time + if (is_array($userInfo)) { + if (!isset($userInfo['user_id'])) { + throw new \LogicException('if $user_id argument is an array, user_id index must be set'); + } + + $auth_time = isset($userInfo['auth_time']) ? $userInfo['auth_time'] : null; + $user_id = $userInfo['user_id']; + } else { + $user_id = $userInfo; + } + + if (is_null($auth_time)) { + $auth_time = time(); + } + + // userInfo is a scalar, and so this is the $user_id. Auth Time is null + return array($user_id, $auth_time); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php new file mode 100644 index 000000000..0bd2f8391 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php @@ -0,0 +1,29 @@ +<?php + +namespace OAuth2\OpenID\ResponseType; + +use OAuth2\ResponseType\ResponseTypeInterface; + +interface IdTokenInterface extends ResponseTypeInterface +{ + /** + * Create the id token. + * + * If Authorization Code Flow is used, the id_token is generated when the + * authorization code is issued, and later returned from the token endpoint + * together with the access_token. + * If the Implicit Flow is used, the token and id_token are generated and + * returned together. + * + * @param string $client_id The client id. + * @param string $user_id The user id. + * @param string $nonce OPTIONAL The nonce. + * @param string $userClaims OPTIONAL Claims about the user. + * @param string $access_token OPTIONAL The access token, if known. + * + * @return string The ID Token represented as a JSON Web Token (JWT). + * + * @see http://openid.net/specs/openid-connect-core-1_0.html#IDToken + */ + public function createIdToken($client_id, $userInfo, $nonce = null, $userClaims = null, $access_token = null); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenToken.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenToken.php new file mode 100644 index 000000000..f0c59799b --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenToken.php @@ -0,0 +1,27 @@ +<?php + +namespace OAuth2\OpenID\ResponseType; + +use OAuth2\ResponseType\AccessTokenInterface; + +class IdTokenToken implements IdTokenTokenInterface +{ + protected $accessToken; + protected $idToken; + + public function __construct(AccessTokenInterface $accessToken, IdTokenInterface $idToken) + { + $this->accessToken = $accessToken; + $this->idToken = $idToken; + } + + public function getAuthorizeResponse($params, $user_id = null) + { + $result = $this->accessToken->getAuthorizeResponse($params, $user_id); + $access_token = $result[1]['fragment']['access_token']; + $id_token = $this->idToken->createIdToken($params['client_id'], $user_id, $params['nonce'], null, $access_token); + $result[1]['fragment']['id_token'] = $id_token; + + return $result; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php new file mode 100644 index 000000000..ac13e2032 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php @@ -0,0 +1,9 @@ +<?php + +namespace OAuth2\OpenID\ResponseType; + +use OAuth2\ResponseType\ResponseTypeInterface; + +interface IdTokenTokenInterface extends ResponseTypeInterface +{ +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php new file mode 100644 index 000000000..51dd867ec --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php @@ -0,0 +1,37 @@ +<?php + +namespace OAuth2\OpenID\Storage; + +use OAuth2\Storage\AuthorizationCodeInterface as BaseAuthorizationCodeInterface; +/** + * Implement this interface to specify where the OAuth2 Server + * should get/save authorization codes for the "Authorization Code" + * grant type + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +interface AuthorizationCodeInterface extends BaseAuthorizationCodeInterface +{ + /** + * Take the provided authorization code values and store them somewhere. + * + * This function should be the storage counterpart to getAuthCode(). + * + * If storage fails for some reason, we're not currently checking for + * any sort of success/failure, so you should bail out of the script + * and provide a descriptive fail message. + * + * Required for OAuth2::GRANT_TYPE_AUTH_CODE. + * + * @param $code authorization code to be stored. + * @param $client_id client identifier to be stored. + * @param $user_id user identifier to be stored. + * @param string $redirect_uri redirect URI(s) to be stored in a space-separated string. + * @param int $expires expiration to be stored as a Unix timestamp. + * @param string $scope OPTIONAL scopes to be stored in space-separated string. + * @param string $id_token OPTIONAL the OpenID Connect id_token. + * + * @ingroup oauth2_section_4 + */ + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Storage/UserClaimsInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Storage/UserClaimsInterface.php new file mode 100644 index 000000000..f230bef9e --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Storage/UserClaimsInterface.php @@ -0,0 +1,38 @@ +<?php + +namespace OAuth2\OpenID\Storage; + +/** + * Implement this interface to specify where the OAuth2 Server + * should retrieve user claims for the OpenID Connect id_token. + */ +interface UserClaimsInterface +{ + // valid scope values to pass into the user claims API call + const VALID_CLAIMS = 'profile email address phone'; + + // fields returned for the claims above + const PROFILE_CLAIM_VALUES = 'name family_name given_name middle_name nickname preferred_username profile picture website gender birthdate zoneinfo locale updated_at'; + const EMAIL_CLAIM_VALUES = 'email email_verified'; + const ADDRESS_CLAIM_VALUES = 'formatted street_address locality region postal_code country'; + const PHONE_CLAIM_VALUES = 'phone_number phone_number_verified'; + + /** + * Return claims about the provided user id. + * + * Groups of claims are returned based on the requested scopes. No group + * is required, and no claim is required. + * + * @param $user_id + * The id of the user for which claims should be returned. + * @param $scope + * The requested scope. + * Scopes with matching claims: profile, email, address, phone. + * + * @return + * An array in the claim => value format. + * + * @see http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + */ + public function getUserClaims($user_id, $scope); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Request.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Request.php new file mode 100644 index 000000000..c92cee821 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Request.php @@ -0,0 +1,213 @@ +<?php + +namespace OAuth2; + +/** + * OAuth2\Request + * This class is taken from the Symfony2 Framework and is part of the Symfony package. + * See Symfony\Component\HttpFoundation\Request (https://github.com/symfony/symfony) + */ +class Request implements RequestInterface +{ + public $attributes; + public $request; + public $query; + public $server; + public $files; + public $cookies; + public $headers; + public $content; + + /** + * Constructor. + * + * @param array $query The GET parameters + * @param array $request The POST parameters + * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies The COOKIE parameters + * @param array $files The FILES parameters + * @param array $server The SERVER parameters + * @param string $content The raw body data + * + * @api + */ + public function __construct(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null, array $headers = null) + { + $this->initialize($query, $request, $attributes, $cookies, $files, $server, $content, $headers); + } + + /** + * Sets the parameters for this request. + * + * This method also re-initializes all properties. + * + * @param array $query The GET parameters + * @param array $request The POST parameters + * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies The COOKIE parameters + * @param array $files The FILES parameters + * @param array $server The SERVER parameters + * @param string $content The raw body data + * + * @api + */ + public function initialize(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null, array $headers = null) + { + $this->request = $request; + $this->query = $query; + $this->attributes = $attributes; + $this->cookies = $cookies; + $this->files = $files; + $this->server = $server; + $this->content = $content; + $this->headers = is_null($headers) ? $this->getHeadersFromServer($this->server) : $headers; + } + + public function query($name, $default = null) + { + return isset($this->query[$name]) ? $this->query[$name] : $default; + } + + public function request($name, $default = null) + { + return isset($this->request[$name]) ? $this->request[$name] : $default; + } + + public function server($name, $default = null) + { + return isset($this->server[$name]) ? $this->server[$name] : $default; + } + + public function headers($name, $default = null) + { + $headers = array_change_key_case($this->headers); + $name = strtolower($name); + + return isset($headers[$name]) ? $headers[$name] : $default; + } + + public function getAllQueryParameters() + { + return $this->query; + } + + /** + * Returns the request body content. + * + * @param Boolean $asResource If true, a resource will be returned + * + * @return string|resource The request body content or a resource to read the body stream. + */ + public function getContent($asResource = false) + { + if (false === $this->content || (true === $asResource && null !== $this->content)) { + throw new \LogicException('getContent() can only be called once when using the resource return type.'); + } + + if (true === $asResource) { + $this->content = false; + + return fopen('php://input', 'rb'); + } + + if (null === $this->content) { + $this->content = file_get_contents('php://input'); + } + + return $this->content; + } + + private function getHeadersFromServer($server) + { + $headers = array(); + foreach ($server as $key => $value) { + if (0 === strpos($key, 'HTTP_')) { + $headers[substr($key, 5)] = $value; + } + // CONTENT_* are not prefixed with HTTP_ + elseif (in_array($key, array('CONTENT_LENGTH', 'CONTENT_MD5', 'CONTENT_TYPE'))) { + $headers[$key] = $value; + } + } + + if (isset($server['PHP_AUTH_USER'])) { + $headers['PHP_AUTH_USER'] = $server['PHP_AUTH_USER']; + $headers['PHP_AUTH_PW'] = isset($server['PHP_AUTH_PW']) ? $server['PHP_AUTH_PW'] : ''; + } else { + /* + * php-cgi under Apache does not pass HTTP Basic user/pass to PHP by default + * For this workaround to work, add this line to your .htaccess file: + * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + * + * A sample .htaccess file: + * RewriteEngine On + * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + * RewriteCond %{REQUEST_FILENAME} !-f + * RewriteRule ^(.*)$ app.php [QSA,L] + */ + + $authorizationHeader = null; + if (isset($server['HTTP_AUTHORIZATION'])) { + $authorizationHeader = $server['HTTP_AUTHORIZATION']; + } elseif (isset($server['REDIRECT_HTTP_AUTHORIZATION'])) { + $authorizationHeader = $server['REDIRECT_HTTP_AUTHORIZATION']; + } elseif (function_exists('apache_request_headers')) { + $requestHeaders = (array) apache_request_headers(); + + // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization) + $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders)); + + if (isset($requestHeaders['Authorization'])) { + $authorizationHeader = trim($requestHeaders['Authorization']); + } + } + + if (null !== $authorizationHeader) { + $headers['AUTHORIZATION'] = $authorizationHeader; + // Decode AUTHORIZATION header into PHP_AUTH_USER and PHP_AUTH_PW when authorization header is basic + if (0 === stripos($authorizationHeader, 'basic')) { + $exploded = explode(':', base64_decode(substr($authorizationHeader, 6))); + if (count($exploded) == 2) { + list($headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']) = $exploded; + } + } + } + } + + // PHP_AUTH_USER/PHP_AUTH_PW + if (isset($headers['PHP_AUTH_USER'])) { + $headers['AUTHORIZATION'] = 'Basic '.base64_encode($headers['PHP_AUTH_USER'].':'.$headers['PHP_AUTH_PW']); + } + + return $headers; + } + + /** + * Creates a new request with values from PHP's super globals. + * + * @return Request A new request + * + * @api + */ + public static function createFromGlobals() + { + $class = get_called_class(); + $request = new $class($_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER); + + $contentType = $request->server('CONTENT_TYPE', ''); + $requestMethod = $request->server('REQUEST_METHOD', 'GET'); + if (0 === strpos($contentType, 'application/x-www-form-urlencoded') + && in_array(strtoupper($requestMethod), array('PUT', 'DELETE')) + ) { + parse_str($request->getContent(), $data); + $request->request = $data; + } elseif (0 === strpos($contentType, 'application/json') + && in_array(strtoupper($requestMethod), array('POST', 'PUT', 'DELETE')) + ) { + $data = json_decode($request->getContent(), true); + $request->request = $data; + } + + return $request; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/RequestInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/RequestInterface.php new file mode 100644 index 000000000..8a70d5fad --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/RequestInterface.php @@ -0,0 +1,16 @@ +<?php + +namespace OAuth2; + +interface RequestInterface +{ + public function query($name, $default = null); + + public function request($name, $default = null); + + public function server($name, $default = null); + + public function headers($name, $default = null); + + public function getAllQueryParameters(); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Response.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Response.php new file mode 100644 index 000000000..fc1e62a98 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Response.php @@ -0,0 +1,369 @@ +<?php + +namespace OAuth2; + +/** + * Class to handle OAuth2 Responses in a graceful way. Use this interface + * to output the proper OAuth2 responses. + * + * @see OAuth2\ResponseInterface + * + * This class borrows heavily from the Symfony2 Framework and is part of the symfony package + * @see Symfony\Component\HttpFoundation\Request (https://github.com/symfony/symfony) + */ +class Response implements ResponseInterface +{ + public $version; + protected $statusCode = 200; + protected $statusText; + protected $parameters = array(); + protected $httpHeaders = array(); + + public static $statusTexts = array( + 100 => 'Continue', + 101 => 'Switching Protocols', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + ); + + public function __construct($parameters = array(), $statusCode = 200, $headers = array()) + { + $this->setParameters($parameters); + $this->setStatusCode($statusCode); + $this->setHttpHeaders($headers); + $this->version = '1.1'; + } + + /** + * Converts the response object to string containing all headers and the response content. + * + * @return string The response with headers and content + */ + public function __toString() + { + $headers = array(); + foreach ($this->httpHeaders as $name => $value) { + $headers[$name] = (array) $value; + } + + return + sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n". + $this->getHttpHeadersAsString($headers)."\r\n". + $this->getResponseBody(); + } + + /** + * Returns the build header line. + * + * @param string $name The header name + * @param string $value The header value + * + * @return string The built header line + */ + protected function buildHeader($name, $value) + { + return sprintf("%s: %s\n", $name, $value); + } + + public function getStatusCode() + { + return $this->statusCode; + } + + public function setStatusCode($statusCode, $text = null) + { + $this->statusCode = (int) $statusCode; + if ($this->isInvalid()) { + throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $statusCode)); + } + + $this->statusText = false === $text ? '' : (null === $text ? self::$statusTexts[$this->statusCode] : $text); + } + + public function getStatusText() + { + return $this->statusText; + } + + public function getParameters() + { + return $this->parameters; + } + + public function setParameters(array $parameters) + { + $this->parameters = $parameters; + } + + public function addParameters(array $parameters) + { + $this->parameters = array_merge($this->parameters, $parameters); + } + + public function getParameter($name, $default = null) + { + return isset($this->parameters[$name]) ? $this->parameters[$name] : $default; + } + + public function setParameter($name, $value) + { + $this->parameters[$name] = $value; + } + + public function setHttpHeaders(array $httpHeaders) + { + $this->httpHeaders = $httpHeaders; + } + + public function setHttpHeader($name, $value) + { + $this->httpHeaders[$name] = $value; + } + + public function addHttpHeaders(array $httpHeaders) + { + $this->httpHeaders = array_merge($this->httpHeaders, $httpHeaders); + } + + public function getHttpHeaders() + { + return $this->httpHeaders; + } + + public function getHttpHeader($name, $default = null) + { + return isset($this->httpHeaders[$name]) ? $this->httpHeaders[$name] : $default; + } + + public function getResponseBody($format = 'json') + { + switch ($format) { + case 'json': + return $this->parameters ? json_encode($this->parameters) : ''; + case 'xml': + // this only works for single-level arrays + $xml = new \SimpleXMLElement('<response/>'); + foreach ($this->parameters as $key => $param) { + $xml->addChild($key, $param); + } + + return $xml->asXML(); + } + + throw new \InvalidArgumentException(sprintf('The format %s is not supported', $format)); + + } + + public function send($format = 'json') + { + // headers have already been sent by the developer + if (headers_sent()) { + return; + } + + switch ($format) { + case 'json': + $this->setHttpHeader('Content-Type', 'application/json'); + break; + case 'xml': + $this->setHttpHeader('Content-Type', 'text/xml'); + break; + } + // status + header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)); + + foreach ($this->getHttpHeaders() as $name => $header) { + header(sprintf('%s: %s', $name, $header)); + } + echo $this->getResponseBody($format); + } + + public function setError($statusCode, $error, $errorDescription = null, $errorUri = null) + { + $parameters = array( + 'error' => $error, + 'error_description' => $errorDescription, + ); + + if (!is_null($errorUri)) { + if (strlen($errorUri) > 0 && $errorUri[0] == '#') { + // we are referencing an oauth bookmark (for brevity) + $errorUri = 'http://tools.ietf.org/html/rfc6749' . $errorUri; + } + $parameters['error_uri'] = $errorUri; + } + + $httpHeaders = array( + 'Cache-Control' => 'no-store' + ); + + $this->setStatusCode($statusCode); + $this->addParameters($parameters); + $this->addHttpHeaders($httpHeaders); + + if (!$this->isClientError() && !$this->isServerError()) { + throw new \InvalidArgumentException(sprintf('The HTTP status code is not an error ("%s" given).', $statusCode)); + } + } + + public function setRedirect($statusCode, $url, $state = null, $error = null, $errorDescription = null, $errorUri = null) + { + if (empty($url)) { + throw new \InvalidArgumentException('Cannot redirect to an empty URL.'); + } + + $parameters = array(); + + if (!is_null($state)) { + $parameters['state'] = $state; + } + + if (!is_null($error)) { + $this->setError(400, $error, $errorDescription, $errorUri); + } + $this->setStatusCode($statusCode); + $this->addParameters($parameters); + + if (count($this->parameters) > 0) { + // add parameters to URL redirection + $parts = parse_url($url); + $sep = isset($parts['query']) && count($parts['query']) > 0 ? '&' : '?'; + $url .= $sep . http_build_query($this->parameters); + } + + $this->addHttpHeaders(array('Location' => $url)); + + if (!$this->isRedirection()) { + throw new \InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $statusCode)); + } + } + + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + /** + * @return Boolean + * + * @api + */ + public function isInvalid() + { + return $this->statusCode < 100 || $this->statusCode >= 600; + } + + /** + * @return Boolean + * + * @api + */ + public function isInformational() + { + return $this->statusCode >= 100 && $this->statusCode < 200; + } + + /** + * @return Boolean + * + * @api + */ + public function isSuccessful() + { + return $this->statusCode >= 200 && $this->statusCode < 300; + } + + /** + * @return Boolean + * + * @api + */ + public function isRedirection() + { + return $this->statusCode >= 300 && $this->statusCode < 400; + } + + /** + * @return Boolean + * + * @api + */ + public function isClientError() + { + return $this->statusCode >= 400 && $this->statusCode < 500; + } + + /** + * @return Boolean + * + * @api + */ + public function isServerError() + { + return $this->statusCode >= 500 && $this->statusCode < 600; + } + + /* + * Functions from Symfony2 HttpFoundation - output pretty header + */ + private function getHttpHeadersAsString($headers) + { + if (count($headers) == 0) { + return ''; + } + + $max = max(array_map('strlen', array_keys($headers))) + 1; + $content = ''; + ksort($headers); + foreach ($headers as $name => $values) { + foreach ($values as $value) { + $content .= sprintf("%-{$max}s %s\r\n", $this->beautifyHeaderName($name).':', $value); + } + } + + return $content; + } + + private function beautifyHeaderName($name) + { + return preg_replace_callback('/\-(.)/', array($this, 'beautifyCallback'), ucfirst($name)); + } + + private function beautifyCallback($match) + { + return '-'.strtoupper($match[1]); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseInterface.php new file mode 100644 index 000000000..c99b5f7d1 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseInterface.php @@ -0,0 +1,24 @@ +<?php + +namespace OAuth2; + +/** + * Interface which represents an object response. Meant to handle and display the proper OAuth2 Responses + * for errors and successes + * + * @see OAuth2\Response + */ +interface ResponseInterface +{ + public function addParameters(array $parameters); + + public function addHttpHeaders(array $httpHeaders); + + public function setStatusCode($statusCode); + + public function setError($statusCode, $name, $description = null, $uri = null); + + public function setRedirect($statusCode, $url, $state = null, $error = null, $errorDescription = null, $errorUri = null); + + public function getParameter($name); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AccessToken.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AccessToken.php new file mode 100644 index 000000000..98f51218f --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AccessToken.php @@ -0,0 +1,194 @@ +<?php + +namespace OAuth2\ResponseType; + +use OAuth2\Storage\AccessTokenInterface as AccessTokenStorageInterface; +use OAuth2\Storage\RefreshTokenInterface; + +/** + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +class AccessToken implements AccessTokenInterface +{ + protected $tokenStorage; + protected $refreshStorage; + protected $config; + + /** + * @param OAuth2\Storage\AccessTokenInterface $tokenStorage REQUIRED Storage class for saving access token information + * @param OAuth2\Storage\RefreshTokenInterface $refreshStorage OPTIONAL Storage class for saving refresh token information + * @param array $config OPTIONAL Configuration options for the server + * <code> + * $config = array( + * 'token_type' => 'bearer', // token type identifier + * 'access_lifetime' => 3600, // time before access token expires + * 'refresh_token_lifetime' => 1209600, // time before refresh token expires + * ); + * </endcode> + */ + public function __construct(AccessTokenStorageInterface $tokenStorage, RefreshTokenInterface $refreshStorage = null, array $config = array()) + { + $this->tokenStorage = $tokenStorage; + $this->refreshStorage = $refreshStorage; + + $this->config = array_merge(array( + 'token_type' => 'bearer', + 'access_lifetime' => 3600, + 'refresh_token_lifetime' => 1209600, + ), $config); + } + + public function getAuthorizeResponse($params, $user_id = null) + { + // build the URL to redirect to + $result = array('query' => array()); + + $params += array('scope' => null, 'state' => null); + + /* + * a refresh token MUST NOT be included in the fragment + * + * @see http://tools.ietf.org/html/rfc6749#section-4.2.2 + */ + $includeRefreshToken = false; + $result["fragment"] = $this->createAccessToken($params['client_id'], $user_id, $params['scope'], $includeRefreshToken); + + if (isset($params['state'])) { + $result["fragment"]["state"] = $params['state']; + } + + return array($params['redirect_uri'], $result); + } + + /** + * Handle the creation of access token, also issue refresh token if supported / desirable. + * + * @param $client_id client identifier related to the access token. + * @param $user_id user ID associated with the access token + * @param $scope OPTIONAL scopes to be stored in space-separated string. + * @param bool $includeRefreshToken if true, a new refresh_token will be added to the response + * + * @see http://tools.ietf.org/html/rfc6749#section-5 + * @ingroup oauth2_section_5 + */ + public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true) + { + $token = array( + "access_token" => $this->generateAccessToken(), + "expires_in" => $this->config['access_lifetime'], + "token_type" => $this->config['token_type'], + "scope" => $scope + ); + + $this->tokenStorage->setAccessToken($token["access_token"], $client_id, $user_id, $this->config['access_lifetime'] ? time() + $this->config['access_lifetime'] : null, $scope); + + /* + * Issue a refresh token also, if we support them + * + * Refresh Tokens are considered supported if an instance of OAuth2\Storage\RefreshTokenInterface + * is supplied in the constructor + */ + if ($includeRefreshToken && $this->refreshStorage) { + $token["refresh_token"] = $this->generateRefreshToken(); + $expires = 0; + if ($this->config['refresh_token_lifetime'] > 0) { + $expires = time() + $this->config['refresh_token_lifetime']; + } + $this->refreshStorage->setRefreshToken($token['refresh_token'], $client_id, $user_id, $expires, $scope); + } + + return $token; + } + + /** + * Generates an unique access token. + * + * Implementing classes may want to override this function to implement + * other access token generation schemes. + * + * @return + * An unique access token. + * + * @ingroup oauth2_section_4 + */ + protected function generateAccessToken() + { + if (function_exists('openssl_random_pseudo_bytes')) { + $randomData = openssl_random_pseudo_bytes(20); + if ($randomData !== false && strlen($randomData) === 20) { + return bin2hex($randomData); + } + } + if (function_exists('mcrypt_create_iv')) { + $randomData = mcrypt_create_iv(20, MCRYPT_DEV_URANDOM); + if ($randomData !== false && strlen($randomData) === 20) { + return bin2hex($randomData); + } + } + if (@file_exists('/dev/urandom')) { // Get 100 bytes of random data + $randomData = file_get_contents('/dev/urandom', false, null, 0, 20); + if ($randomData !== false && strlen($randomData) === 20) { + return bin2hex($randomData); + } + } + // Last resort which you probably should just get rid of: + $randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true); + + return substr(hash('sha512', $randomData), 0, 40); + } + + /** + * Generates an unique refresh token + * + * Implementing classes may want to override this function to implement + * other refresh token generation schemes. + * + * @return + * An unique refresh. + * + * @ingroup oauth2_section_4 + * @see OAuth2::generateAccessToken() + */ + protected function generateRefreshToken() + { + return $this->generateAccessToken(); // let's reuse the same scheme for token generation + } + + /** + * Handle the revoking of refresh tokens, and access tokens if supported / desirable + * RFC7009 specifies that "If the server is unable to locate the token using + * the given hint, it MUST extend its search across all of its supported token types" + * + * @param $token + * @param null $tokenTypeHint + * @return boolean + */ + public function revokeToken($token, $tokenTypeHint = null) + { + if ($tokenTypeHint == 'refresh_token') { + if ($this->refreshStorage && $revoked = $this->refreshStorage->unsetRefreshToken($token)) { + return true; + } + } + + /** @TODO remove in v2 */ + if (!method_exists($this->tokenStorage, 'unsetAccessToken')) { + throw new \RuntimeException( + sprintf('Token storage %s must implement unsetAccessToken method', get_class($this->tokenStorage) + )); + } + + $revoked = $this->tokenStorage->unsetAccessToken($token); + + // if a typehint is supplied and fails, try other storages + // @see https://tools.ietf.org/html/rfc7009#section-2.1 + if (!$revoked && $tokenTypeHint != 'refresh_token') { + if ($this->refreshStorage) { + $revoked = $this->refreshStorage->unsetRefreshToken($token); + } + } + + return $revoked; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AccessTokenInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AccessTokenInterface.php new file mode 100644 index 000000000..4bd3928d8 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AccessTokenInterface.php @@ -0,0 +1,34 @@ +<?php + +namespace OAuth2\ResponseType; + +/** + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +interface AccessTokenInterface extends ResponseTypeInterface +{ + /** + * Handle the creation of access token, also issue refresh token if supported / desirable. + * + * @param $client_id client identifier related to the access token. + * @param $user_id user ID associated with the access token + * @param $scope OPTONAL scopes to be stored in space-separated string. + * @param bool $includeRefreshToken if true, a new refresh_token will be added to the response + * + * @see http://tools.ietf.org/html/rfc6749#section-5 + * @ingroup oauth2_section_5 + */ + public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true); + + /** + * Handle the revoking of refresh tokens, and access tokens if supported / desirable + * + * @param $token + * @param $tokenTypeHint + * @return mixed + * + * @todo v2.0 include this method in interface. Omitted to maintain BC in v1.x + */ + //public function revokeToken($token, $tokenTypeHint); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCode.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCode.php new file mode 100644 index 000000000..52aeb4be5 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCode.php @@ -0,0 +1,100 @@ +<?php + +namespace OAuth2\ResponseType; + +use OAuth2\Storage\AuthorizationCodeInterface as AuthorizationCodeStorageInterface; + +/** + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +class AuthorizationCode implements AuthorizationCodeInterface +{ + protected $storage; + protected $config; + + public function __construct(AuthorizationCodeStorageInterface $storage, array $config = array()) + { + $this->storage = $storage; + $this->config = array_merge(array( + 'enforce_redirect' => false, + 'auth_code_lifetime' => 30, + ), $config); + } + + public function getAuthorizeResponse($params, $user_id = null) + { + // build the URL to redirect to + $result = array('query' => array()); + + $params += array('scope' => null, 'state' => null); + + $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope']); + + if (isset($params['state'])) { + $result['query']['state'] = $params['state']; + } + + return array($params['redirect_uri'], $result); + } + + /** + * Handle the creation of the authorization code. + * + * @param $client_id + * Client identifier related to the authorization code + * @param $user_id + * User ID associated with the authorization code + * @param $redirect_uri + * An absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @ingroup oauth2_section_4 + */ + public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null) + { + $code = $this->generateAuthorizationCode(); + $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope); + + return $code; + } + + /** + * @return + * TRUE if the grant type requires a redirect_uri, FALSE if not + */ + public function enforceRedirect() + { + return $this->config['enforce_redirect']; + } + + /** + * Generates an unique auth code. + * + * Implementing classes may want to override this function to implement + * other auth code generation schemes. + * + * @return + * An unique auth code. + * + * @ingroup oauth2_section_4 + */ + protected function generateAuthorizationCode() + { + $tokenLen = 40; + if (function_exists('openssl_random_pseudo_bytes')) { + $randomData = openssl_random_pseudo_bytes(100); + } elseif (function_exists('mcrypt_create_iv')) { + $randomData = mcrypt_create_iv(100, MCRYPT_DEV_URANDOM); + } elseif (@file_exists('/dev/urandom')) { // Get 100 bytes of random data + $randomData = file_get_contents('/dev/urandom', false, null, 0, 100) . uniqid(mt_rand(), true); + } else { + $randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true); + } + + return substr(hash('sha512', $randomData), 0, $tokenLen); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCodeInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCodeInterface.php new file mode 100644 index 000000000..df777e221 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCodeInterface.php @@ -0,0 +1,30 @@ +<?php + +namespace OAuth2\ResponseType; + +/** + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +interface AuthorizationCodeInterface extends ResponseTypeInterface +{ + /** + * @return + * TRUE if the grant type requires a redirect_uri, FALSE if not + */ + public function enforceRedirect(); + + /** + * Handle the creation of the authorization code. + * + * @param $client_id client identifier related to the authorization code + * @param $user_id user id associated with the authorization code + * @param $redirect_uri an absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param $scope OPTIONAL scopes to be stored in space-separated string. + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @ingroup oauth2_section_4 + */ + public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/JwtAccessToken.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/JwtAccessToken.php new file mode 100644 index 000000000..3942fe41e --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/JwtAccessToken.php @@ -0,0 +1,124 @@ +<?php + +namespace OAuth2\ResponseType; + +use OAuth2\Encryption\EncryptionInterface; +use OAuth2\Encryption\Jwt; +use OAuth2\Storage\AccessTokenInterface as AccessTokenStorageInterface; +use OAuth2\Storage\RefreshTokenInterface; +use OAuth2\Storage\PublicKeyInterface; +use OAuth2\Storage\Memory; + +/** + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +class JwtAccessToken extends AccessToken +{ + protected $publicKeyStorage; + protected $encryptionUtil; + + /** + * @param $config + * - store_encrypted_token_string (bool true) + * whether the entire encrypted string is stored, + * or just the token ID is stored + */ + public function __construct(PublicKeyInterface $publicKeyStorage = null, AccessTokenStorageInterface $tokenStorage = null, RefreshTokenInterface $refreshStorage = null, array $config = array(), EncryptionInterface $encryptionUtil = null) + { + $this->publicKeyStorage = $publicKeyStorage; + $config = array_merge(array( + 'store_encrypted_token_string' => true, + 'issuer' => '' + ), $config); + if (is_null($tokenStorage)) { + // a pass-thru, so we can call the parent constructor + $tokenStorage = new Memory(); + } + if (is_null($encryptionUtil)) { + $encryptionUtil = new Jwt(); + } + $this->encryptionUtil = $encryptionUtil; + parent::__construct($tokenStorage, $refreshStorage, $config); + } + + /** + * Handle the creation of access token, also issue refresh token if supported / desirable. + * + * @param $client_id + * Client identifier related to the access token. + * @param $user_id + * User ID associated with the access token + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * @param bool $includeRefreshToken + * If true, a new refresh_token will be added to the response + * + * @see http://tools.ietf.org/html/rfc6749#section-5 + * @ingroup oauth2_section_5 + */ + public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true) + { + // token to encrypt + $expires = time() + $this->config['access_lifetime']; + $id = $this->generateAccessToken(); + $jwtAccessToken = array( + 'id' => $id, // for BC (see #591) + 'jti' => $id, + 'iss' => $this->config['issuer'], + 'aud' => $client_id, + 'sub' => $user_id, + 'exp' => $expires, + 'iat' => time(), + 'token_type' => $this->config['token_type'], + 'scope' => $scope + ); + + /* + * Encode the token data into a single access_token string + */ + $access_token = $this->encodeToken($jwtAccessToken, $client_id); + + /* + * Save the token to a secondary storage. This is implemented on the + * OAuth2\Storage\JwtAccessToken side, and will not actually store anything, + * if no secondary storage has been supplied + */ + $token_to_store = $this->config['store_encrypted_token_string'] ? $access_token : $jwtAccessToken['id']; + $this->tokenStorage->setAccessToken($token_to_store, $client_id, $user_id, $this->config['access_lifetime'] ? time() + $this->config['access_lifetime'] : null, $scope); + + // token to return to the client + $token = array( + 'access_token' => $access_token, + 'expires_in' => $this->config['access_lifetime'], + 'token_type' => $this->config['token_type'], + 'scope' => $scope + ); + + /* + * Issue a refresh token also, if we support them + * + * Refresh Tokens are considered supported if an instance of OAuth2\Storage\RefreshTokenInterface + * is supplied in the constructor + */ + if ($includeRefreshToken && $this->refreshStorage) { + $refresh_token = $this->generateRefreshToken(); + $expires = 0; + if ($this->config['refresh_token_lifetime'] > 0) { + $expires = time() + $this->config['refresh_token_lifetime']; + } + $this->refreshStorage->setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope); + $token['refresh_token'] = $refresh_token; + } + + return $token; + } + + protected function encodeToken(array $token, $client_id = null) + { + $private_key = $this->publicKeyStorage->getPrivateKey($client_id); + $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); + + return $this->encryptionUtil->encode($token, $private_key, $algorithm); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/ResponseTypeInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/ResponseTypeInterface.php new file mode 100644 index 000000000..f8e26a5b0 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/ResponseTypeInterface.php @@ -0,0 +1,8 @@ +<?php + +namespace OAuth2\ResponseType; + +interface ResponseTypeInterface +{ + public function getAuthorizeResponse($params, $user_id = null); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Scope.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Scope.php new file mode 100644 index 000000000..c44350bfd --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Scope.php @@ -0,0 +1,103 @@ +<?php + +namespace OAuth2; + +use OAuth2\Storage\Memory; +use OAuth2\Storage\ScopeInterface as ScopeStorageInterface; + +/** +* @see OAuth2\ScopeInterface +*/ +class Scope implements ScopeInterface +{ + protected $storage; + + /** + * @param mixed @storage + * Either an array of supported scopes, or an instance of OAuth2\Storage\ScopeInterface + */ + public function __construct($storage = null) + { + if (is_null($storage) || is_array($storage)) { + $storage = new Memory((array) $storage); + } + + if (!$storage instanceof ScopeStorageInterface) { + throw new \InvalidArgumentException("Argument 1 to OAuth2\Scope must be null, an array, or instance of OAuth2\Storage\ScopeInterface"); + } + + $this->storage = $storage; + } + + /** + * Check if everything in required scope is contained in available scope. + * + * @param $required_scope + * A space-separated string of scopes. + * + * @return + * TRUE if everything in required scope is contained in available scope, + * and FALSE if it isn't. + * + * @see http://tools.ietf.org/html/rfc6749#section-7 + * + * @ingroup oauth2_section_7 + */ + public function checkScope($required_scope, $available_scope) + { + $required_scope = explode(' ', trim($required_scope)); + $available_scope = explode(' ', trim($available_scope)); + + return (count(array_diff($required_scope, $available_scope)) == 0); + } + + /** + * Check if the provided scope exists in storage. + * + * @param $scope + * A space-separated string of scopes. + * + * @return + * TRUE if it exists, FALSE otherwise. + */ + public function scopeExists($scope) + { + // Check reserved scopes first. + $scope = explode(' ', trim($scope)); + $reservedScope = $this->getReservedScopes(); + $nonReservedScopes = array_diff($scope, $reservedScope); + if (count($nonReservedScopes) == 0) { + return true; + } else { + // Check the storage for non-reserved scopes. + $nonReservedScopes = implode(' ', $nonReservedScopes); + + return $this->storage->scopeExists($nonReservedScopes); + } + } + + public function getScopeFromRequest(RequestInterface $request) + { + // "scope" is valid if passed in either POST or QUERY + return $request->request('scope', $request->query('scope')); + } + + public function getDefaultScope($client_id = null) + { + return $this->storage->getDefaultScope($client_id); + } + + /** + * Get reserved scopes needed by the server. + * + * In case OpenID Connect is used, these scopes must include: + * 'openid', offline_access'. + * + * @return + * An array of reserved scopes. + */ + public function getReservedScopes() + { + return array('openid', 'offline_access'); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/ScopeInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ScopeInterface.php new file mode 100644 index 000000000..5b60f9aee --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/ScopeInterface.php @@ -0,0 +1,40 @@ +<?php + +namespace OAuth2; + +use OAuth2\Storage\ScopeInterface as ScopeStorageInterface; + +/** + * Class to handle scope implementation logic + * + * @see OAuth2\Storage\ScopeInterface + */ +interface ScopeInterface extends ScopeStorageInterface +{ + /** + * Check if everything in required scope is contained in available scope. + * + * @param $required_scope + * A space-separated string of scopes. + * + * @return + * TRUE if everything in required scope is contained in available scope, + * and FALSE if it isn't. + * + * @see http://tools.ietf.org/html/rfc6749#section-7 + * + * @ingroup oauth2_section_7 + */ + public function checkScope($required_scope, $available_scope); + + /** + * Return scope info from request + * + * @param OAuth2\RequestInterface + * Request object to check + * + * @return + * string representation of requested scope + */ + public function getScopeFromRequest(RequestInterface $request); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Server.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Server.php new file mode 100644 index 000000000..9cfcb83a5 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Server.php @@ -0,0 +1,879 @@ +<?php + +namespace OAuth2; + +use OAuth2\Controller\ResourceControllerInterface; +use OAuth2\Controller\ResourceController; +use OAuth2\OpenID\Controller\UserInfoControllerInterface; +use OAuth2\OpenID\Controller\UserInfoController; +use OAuth2\OpenID\Controller\AuthorizeController as OpenIDAuthorizeController; +use OAuth2\OpenID\ResponseType\AuthorizationCode as OpenIDAuthorizationCodeResponseType; +use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; +use OAuth2\OpenID\GrantType\AuthorizationCode as OpenIDAuthorizationCodeGrantType; +use OAuth2\Controller\AuthorizeControllerInterface; +use OAuth2\Controller\AuthorizeController; +use OAuth2\Controller\TokenControllerInterface; +use OAuth2\Controller\TokenController; +use OAuth2\ClientAssertionType\ClientAssertionTypeInterface; +use OAuth2\ClientAssertionType\HttpBasic; +use OAuth2\ResponseType\ResponseTypeInterface; +use OAuth2\ResponseType\AuthorizationCode as AuthorizationCodeResponseType; +use OAuth2\ResponseType\AccessToken; +use OAuth2\ResponseType\JwtAccessToken; +use OAuth2\OpenID\ResponseType\CodeIdToken; +use OAuth2\OpenID\ResponseType\IdToken; +use OAuth2\OpenID\ResponseType\IdTokenToken; +use OAuth2\TokenType\TokenTypeInterface; +use OAuth2\TokenType\Bearer; +use OAuth2\GrantType\GrantTypeInterface; +use OAuth2\GrantType\UserCredentials; +use OAuth2\GrantType\ClientCredentials; +use OAuth2\GrantType\RefreshToken; +use OAuth2\GrantType\AuthorizationCode; +use OAuth2\Storage\JwtAccessToken as JwtAccessTokenStorage; +use OAuth2\Storage\JwtAccessTokenInterface; + +/** +* Server class for OAuth2 +* This class serves as a convience class which wraps the other Controller classes +* +* @see OAuth2\Controller\ResourceController +* @see OAuth2\Controller\AuthorizeController +* @see OAuth2\Controller\TokenController +*/ +class Server implements ResourceControllerInterface, + AuthorizeControllerInterface, + TokenControllerInterface, + UserInfoControllerInterface +{ + // misc properties + /** + * @var Response + */ + protected $response; + + /** + * @var array + */ + protected $config; + + /** + * @var array + */ + protected $storages; + + // servers + /** + * @var AuthorizeControllerInterface + */ + protected $authorizeController; + + /** + * @var TokenControllerInterface + */ + protected $tokenController; + + /** + * @var ResourceControllerInterface + */ + protected $resourceController; + + /** + * @var UserInfoControllerInterface + */ + protected $userInfoController; + + // config classes + protected $grantTypes; + protected $responseTypes; + protected $tokenType; + + /** + * @var ScopeInterface + */ + protected $scopeUtil; + protected $clientAssertionType; + + protected $storageMap = array( + 'access_token' => 'OAuth2\Storage\AccessTokenInterface', + 'authorization_code' => 'OAuth2\Storage\AuthorizationCodeInterface', + 'client_credentials' => 'OAuth2\Storage\ClientCredentialsInterface', + 'client' => 'OAuth2\Storage\ClientInterface', + 'refresh_token' => 'OAuth2\Storage\RefreshTokenInterface', + 'user_credentials' => 'OAuth2\Storage\UserCredentialsInterface', + 'user_claims' => 'OAuth2\OpenID\Storage\UserClaimsInterface', + 'public_key' => 'OAuth2\Storage\PublicKeyInterface', + 'jwt_bearer' => 'OAuth2\Storage\JWTBearerInterface', + 'scope' => 'OAuth2\Storage\ScopeInterface', + ); + + protected $responseTypeMap = array( + 'token' => 'OAuth2\ResponseType\AccessTokenInterface', + 'code' => 'OAuth2\ResponseType\AuthorizationCodeInterface', + 'id_token' => 'OAuth2\OpenID\ResponseType\IdTokenInterface', + 'id_token token' => 'OAuth2\OpenID\ResponseType\IdTokenTokenInterface', + 'code id_token' => 'OAuth2\OpenID\ResponseType\CodeIdTokenInterface', + ); + + /** + * @param mixed $storage (array or OAuth2\Storage) - single object or array of objects implementing the + * required storage types (ClientCredentialsInterface and AccessTokenInterface as a minimum) + * @param array $config specify a different token lifetime, token header name, etc + * @param array $grantTypes An array of OAuth2\GrantType\GrantTypeInterface to use for granting access tokens + * @param array $responseTypes Response types to use. array keys should be "code" and and "token" for + * Access Token and Authorization Code response types + * @param \OAuth2\TokenType\TokenTypeInterface $tokenType The token type object to use. Valid token types are "bearer" and "mac" + * @param \OAuth2\ScopeInterface $scopeUtil The scope utility class to use to validate scope + * @param \OAuth2\ClientAssertionType\ClientAssertionTypeInterface $clientAssertionType The method in which to verify the client identity. Default is HttpBasic + * + * @ingroup oauth2_section_7 + */ + public function __construct($storage = array(), array $config = array(), array $grantTypes = array(), array $responseTypes = array(), TokenTypeInterface $tokenType = null, ScopeInterface $scopeUtil = null, ClientAssertionTypeInterface $clientAssertionType = null) + { + $storage = is_array($storage) ? $storage : array($storage); + $this->storages = array(); + foreach ($storage as $key => $service) { + $this->addStorage($service, $key); + } + + // merge all config values. These get passed to our controller objects + $this->config = array_merge(array( + 'use_jwt_access_tokens' => false, + 'store_encrypted_token_string' => true, + 'use_openid_connect' => false, + 'id_lifetime' => 3600, + 'access_lifetime' => 3600, + 'www_realm' => 'Service', + 'token_param_name' => 'access_token', + 'token_bearer_header_name' => 'Bearer', + 'enforce_state' => true, + 'require_exact_redirect_uri' => true, + 'allow_implicit' => false, + 'allow_credentials_in_request_body' => true, + 'allow_public_clients' => true, + 'always_issue_new_refresh_token' => false, + 'unset_refresh_token_after_use' => true, + ), $config); + + foreach ($grantTypes as $key => $grantType) { + $this->addGrantType($grantType, $key); + } + + foreach ($responseTypes as $key => $responseType) { + $this->addResponseType($responseType, $key); + } + + $this->tokenType = $tokenType; + $this->scopeUtil = $scopeUtil; + $this->clientAssertionType = $clientAssertionType; + + if ($this->config['use_openid_connect']) { + $this->validateOpenIdConnect(); + } + } + + public function getAuthorizeController() + { + if (is_null($this->authorizeController)) { + $this->authorizeController = $this->createDefaultAuthorizeController(); + } + + return $this->authorizeController; + } + + public function getTokenController() + { + if (is_null($this->tokenController)) { + $this->tokenController = $this->createDefaultTokenController(); + } + + return $this->tokenController; + } + + public function getResourceController() + { + if (is_null($this->resourceController)) { + $this->resourceController = $this->createDefaultResourceController(); + } + + return $this->resourceController; + } + + public function getUserInfoController() + { + if (is_null($this->userInfoController)) { + $this->userInfoController = $this->createDefaultUserInfoController(); + } + + return $this->userInfoController; + } + + /** + * every getter deserves a setter + * + * @param AuthorizeControllerInterface $authorizeController + */ + public function setAuthorizeController(AuthorizeControllerInterface $authorizeController) + { + $this->authorizeController = $authorizeController; + } + + /** + * every getter deserves a setter + * + * @param TokenControllerInterface $tokenController + */ + public function setTokenController(TokenControllerInterface $tokenController) + { + $this->tokenController = $tokenController; + } + + /** + * every getter deserves a setter + * + * @param ResourceControllerInterface $resourceController + */ + public function setResourceController(ResourceControllerInterface $resourceController) + { + $this->resourceController = $resourceController; + } + + /** + * every getter deserves a setter + * + * @param UserInfoControllerInterface $userInfoController + */ + public function setUserInfoController(UserInfoControllerInterface $userInfoController) + { + $this->userInfoController = $userInfoController; + } + + /** + * Return claims about the authenticated end-user. + * This would be called from the "/UserInfo" endpoint as defined in the spec. + * + * @param $request - \OAuth2\RequestInterface + * Request object to grant access token + * + * @param $response - \OAuth2\ResponseInterface + * Response object containing error messages (failure) or user claims (success) + * + * @return ResponseInterface + * + * @throws \InvalidArgumentException + * @throws \LogicException + * + * @see http://openid.net/specs/openid-connect-core-1_0.html#UserInfo + */ + public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $this->getUserInfoController()->handleUserInfoRequest($request, $this->response); + + return $this->response; + } + + /** + * Grant or deny a requested access token. + * This would be called from the "/token" endpoint as defined in the spec. + * Obviously, you can call your endpoint whatever you want. + * + * @param $request - \OAuth2\RequestInterface + * Request object to grant access token + * + * @param $response - \OAuth2\ResponseInterface + * Response object containing error messages (failure) or access token (success) + * + * @return ResponseInterface + * + * @throws \InvalidArgumentException + * @throws \LogicException + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * @see http://tools.ietf.org/html/rfc6749#section-10.6 + * @see http://tools.ietf.org/html/rfc6749#section-4.1.3 + * + * @ingroup oauth2_section_4 + */ + public function handleTokenRequest(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $this->getTokenController()->handleTokenRequest($request, $this->response); + + return $this->response; + } + + public function grantAccessToken(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $value = $this->getTokenController()->grantAccessToken($request, $this->response); + + return $value; + } + + /** + * Handle a revoke token request + * This would be called from the "/revoke" endpoint as defined in the draft Token Revocation spec + * + * @see https://tools.ietf.org/html/rfc7009#section-2 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return Response|ResponseInterface + */ + public function handleRevokeRequest(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $this->getTokenController()->handleRevokeRequest($request, $this->response); + + return $this->response; + } + + /** + * Redirect the user appropriately after approval. + * + * After the user has approved or denied the resource request the + * authorization server should call this function to redirect the user + * appropriately. + * + * @param $request + * The request should have the follow parameters set in the querystring: + * - response_type: The requested response: an access token, an + * authorization code, or both. + * - client_id: The client identifier as described in Section 2. + * - redirect_uri: An absolute URI to which the authorization server + * will redirect the user-agent to when the end-user authorization + * step is completed. + * - scope: (optional) The scope of the resource request expressed as a + * list of space-delimited strings. + * - state: (optional) An opaque value used by the client to maintain + * state between the request and callback. + * @param ResponseInterface $response + * @param $is_authorized + * TRUE or FALSE depending on whether the user authorized the access. + * @param $user_id + * Identifier of user who authorized the client + * + * @return Response + * + * @see http://tools.ietf.org/html/rfc6749#section-4 + * + * @ingroup oauth2_section_4 + */ + public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null) + { + $this->response = $response; + $this->getAuthorizeController()->handleAuthorizeRequest($request, $this->response, $is_authorized, $user_id); + + return $this->response; + } + + /** + * Pull the authorization request data out of the HTTP request. + * - The redirect_uri is OPTIONAL as per draft 20. But your implementation can enforce it + * by setting $config['enforce_redirect'] to true. + * - The state is OPTIONAL but recommended to enforce CSRF. Draft 21 states, however, that + * CSRF protection is MANDATORY. You can enforce this by setting the $config['enforce_state'] to true. + * + * The draft specifies that the parameters should be retrieved from GET, override the Response + * object to change this + * + * @return + * The authorization parameters so the authorization server can prompt + * the user for approval if valid. + * + * @see http://tools.ietf.org/html/rfc6749#section-4.1.1 + * @see http://tools.ietf.org/html/rfc6749#section-10.12 + * + * @ingroup oauth2_section_3 + */ + public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $value = $this->getAuthorizeController()->validateAuthorizeRequest($request, $this->response); + + return $value; + } + + public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response = null, $scope = null) + { + $this->response = is_null($response) ? new Response() : $response; + $value = $this->getResourceController()->verifyResourceRequest($request, $this->response, $scope); + + return $value; + } + + public function getAccessTokenData(RequestInterface $request, ResponseInterface $response = null) + { + $this->response = is_null($response) ? new Response() : $response; + $value = $this->getResourceController()->getAccessTokenData($request, $this->response); + + return $value; + } + + public function addGrantType(GrantTypeInterface $grantType, $identifier = null) + { + if (!is_string($identifier)) { + $identifier = $grantType->getQuerystringIdentifier(); + } + + $this->grantTypes[$identifier] = $grantType; + + // persist added grant type down to TokenController + if (!is_null($this->tokenController)) { + $this->getTokenController()->addGrantType($grantType, $identifier); + } + } + + /** + * Set a storage object for the server + * + * @param $storage + * An object implementing one of the Storage interfaces + * @param $key + * If null, the storage is set to the key of each storage interface it implements + * + * @see storageMap + */ + public function addStorage($storage, $key = null) + { + // if explicitly set to a valid key, do not "magically" set below + if (isset($this->storageMap[$key])) { + if (!is_null($storage) && !$storage instanceof $this->storageMap[$key]) { + throw new \InvalidArgumentException(sprintf('storage of type "%s" must implement interface "%s"', $key, $this->storageMap[$key])); + } + $this->storages[$key] = $storage; + + // special logic to handle "client" and "client_credentials" strangeness + if ($key === 'client' && !isset($this->storages['client_credentials'])) { + if ($storage instanceof \OAuth2\Storage\ClientCredentialsInterface) { + $this->storages['client_credentials'] = $storage; + } + } elseif ($key === 'client_credentials' && !isset($this->storages['client'])) { + if ($storage instanceof \OAuth2\Storage\ClientInterface) { + $this->storages['client'] = $storage; + } + } + } elseif (!is_null($key) && !is_numeric($key)) { + throw new \InvalidArgumentException(sprintf('unknown storage key "%s", must be one of [%s]', $key, implode(', ', array_keys($this->storageMap)))); + } else { + $set = false; + foreach ($this->storageMap as $type => $interface) { + if ($storage instanceof $interface) { + $this->storages[$type] = $storage; + $set = true; + } + } + + if (!$set) { + throw new \InvalidArgumentException(sprintf('storage of class "%s" must implement one of [%s]', get_class($storage), implode(', ', $this->storageMap))); + } + } + } + + public function addResponseType(ResponseTypeInterface $responseType, $key = null) + { + $key = $this->normalizeResponseType($key); + + if (isset($this->responseTypeMap[$key])) { + if (!$responseType instanceof $this->responseTypeMap[$key]) { + throw new \InvalidArgumentException(sprintf('responseType of type "%s" must implement interface "%s"', $key, $this->responseTypeMap[$key])); + } + $this->responseTypes[$key] = $responseType; + } elseif (!is_null($key) && !is_numeric($key)) { + throw new \InvalidArgumentException(sprintf('unknown responseType key "%s", must be one of [%s]', $key, implode(', ', array_keys($this->responseTypeMap)))); + } else { + $set = false; + foreach ($this->responseTypeMap as $type => $interface) { + if ($responseType instanceof $interface) { + $this->responseTypes[$type] = $responseType; + $set = true; + } + } + + if (!$set) { + throw new \InvalidArgumentException(sprintf('Unknown response type %s. Please implement one of [%s]', get_class($responseType), implode(', ', $this->responseTypeMap))); + } + } + } + + public function getScopeUtil() + { + if (!$this->scopeUtil) { + $storage = isset($this->storages['scope']) ? $this->storages['scope'] : null; + $this->scopeUtil = new Scope($storage); + } + + return $this->scopeUtil; + } + + /** + * every getter deserves a setter + * + * @param ScopeInterface $scopeUtil + */ + public function setScopeUtil($scopeUtil) + { + $this->scopeUtil = $scopeUtil; + } + + protected function createDefaultAuthorizeController() + { + if (!isset($this->storages['client'])) { + throw new \LogicException('You must supply a storage object implementing \OAuth2\Storage\ClientInterface to use the authorize server'); + } + if (0 == count($this->responseTypes)) { + $this->responseTypes = $this->getDefaultResponseTypes(); + } + if ($this->config['use_openid_connect'] && !isset($this->responseTypes['id_token'])) { + $this->responseTypes['id_token'] = $this->createDefaultIdTokenResponseType(); + if ($this->config['allow_implicit']) { + $this->responseTypes['id_token token'] = $this->createDefaultIdTokenTokenResponseType(); + } + } + + $config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_implicit enforce_state require_exact_redirect_uri'))); + + if ($this->config['use_openid_connect']) { + return new OpenIDAuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil()); + } + + return new AuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil()); + } + + protected function createDefaultTokenController() + { + if (0 == count($this->grantTypes)) { + $this->grantTypes = $this->getDefaultGrantTypes(); + } + + if (is_null($this->clientAssertionType)) { + // see if HttpBasic assertion type is requred. If so, then create it from storage classes. + foreach ($this->grantTypes as $grantType) { + if (!$grantType instanceof ClientAssertionTypeInterface) { + if (!isset($this->storages['client_credentials'])) { + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\ClientCredentialsInterface to use the token server'); + } + $config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_credentials_in_request_body allow_public_clients'))); + $this->clientAssertionType = new HttpBasic($this->storages['client_credentials'], $config); + break; + } + } + } + + if (!isset($this->storages['client'])) { + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\ClientInterface to use the token server'); + } + + $accessTokenResponseType = $this->getAccessTokenResponseType(); + + return new TokenController($accessTokenResponseType, $this->storages['client'], $this->grantTypes, $this->clientAssertionType, $this->getScopeUtil()); + } + + protected function createDefaultResourceController() + { + if ($this->config['use_jwt_access_tokens']) { + // overwrites access token storage with crypto token storage if "use_jwt_access_tokens" is set + if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof JwtAccessTokenInterface) { + $this->storages['access_token'] = $this->createDefaultJwtAccessTokenStorage(); + } + } elseif (!isset($this->storages['access_token'])) { + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use JwtAccessTokens to use the resource server'); + } + + if (!$this->tokenType) { + $this->tokenType = $this->getDefaultTokenType(); + } + + $config = array_intersect_key($this->config, array('www_realm' => '')); + + return new ResourceController($this->tokenType, $this->storages['access_token'], $config, $this->getScopeUtil()); + } + + protected function createDefaultUserInfoController() + { + if ($this->config['use_jwt_access_tokens']) { + // overwrites access token storage with crypto token storage if "use_jwt_access_tokens" is set + if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof JwtAccessTokenInterface) { + $this->storages['access_token'] = $this->createDefaultJwtAccessTokenStorage(); + } + } elseif (!isset($this->storages['access_token'])) { + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use JwtAccessTokens to use the UserInfo server'); + } + + if (!isset($this->storages['user_claims'])) { + throw new \LogicException('You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use the UserInfo server'); + } + + if (!$this->tokenType) { + $this->tokenType = $this->getDefaultTokenType(); + } + + $config = array_intersect_key($this->config, array('www_realm' => '')); + + return new UserInfoController($this->tokenType, $this->storages['access_token'], $this->storages['user_claims'], $config, $this->getScopeUtil()); + } + + protected function getDefaultTokenType() + { + $config = array_intersect_key($this->config, array_flip(explode(' ', 'token_param_name token_bearer_header_name'))); + + return new Bearer($config); + } + + protected function getDefaultResponseTypes() + { + $responseTypes = array(); + + if ($this->config['allow_implicit']) { + $responseTypes['token'] = $this->getAccessTokenResponseType(); + } + + if ($this->config['use_openid_connect']) { + $responseTypes['id_token'] = $this->getIdTokenResponseType(); + if ($this->config['allow_implicit']) { + $responseTypes['id_token token'] = $this->getIdTokenTokenResponseType(); + } + } + + if (isset($this->storages['authorization_code'])) { + $config = array_intersect_key($this->config, array_flip(explode(' ', 'enforce_redirect auth_code_lifetime'))); + if ($this->config['use_openid_connect']) { + if (!$this->storages['authorization_code'] instanceof OpenIDAuthorizationCodeInterface) { + throw new \LogicException('Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when "use_openid_connect" is true'); + } + $responseTypes['code'] = new OpenIDAuthorizationCodeResponseType($this->storages['authorization_code'], $config); + $responseTypes['code id_token'] = new CodeIdToken($responseTypes['code'], $responseTypes['id_token']); + } else { + $responseTypes['code'] = new AuthorizationCodeResponseType($this->storages['authorization_code'], $config); + } + } + + if (count($responseTypes) == 0) { + throw new \LogicException('You must supply an array of response_types in the constructor or implement a OAuth2\Storage\AuthorizationCodeInterface storage object or set "allow_implicit" to true and implement a OAuth2\Storage\AccessTokenInterface storage object'); + } + + return $responseTypes; + } + + protected function getDefaultGrantTypes() + { + $grantTypes = array(); + + if (isset($this->storages['user_credentials'])) { + $grantTypes['password'] = new UserCredentials($this->storages['user_credentials']); + } + + if (isset($this->storages['client_credentials'])) { + $config = array_intersect_key($this->config, array('allow_credentials_in_request_body' => '')); + $grantTypes['client_credentials'] = new ClientCredentials($this->storages['client_credentials'], $config); + } + + if (isset($this->storages['refresh_token'])) { + $config = array_intersect_key($this->config, array_flip(explode(' ', 'always_issue_new_refresh_token unset_refresh_token_after_use'))); + $grantTypes['refresh_token'] = new RefreshToken($this->storages['refresh_token'], $config); + } + + if (isset($this->storages['authorization_code'])) { + if ($this->config['use_openid_connect']) { + if (!$this->storages['authorization_code'] instanceof OpenIDAuthorizationCodeInterface) { + throw new \LogicException('Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when "use_openid_connect" is true'); + } + $grantTypes['authorization_code'] = new OpenIDAuthorizationCodeGrantType($this->storages['authorization_code']); + } else { + $grantTypes['authorization_code'] = new AuthorizationCode($this->storages['authorization_code']); + } + } + + if (count($grantTypes) == 0) { + throw new \LogicException('Unable to build default grant types - You must supply an array of grant_types in the constructor'); + } + + return $grantTypes; + } + + protected function getAccessTokenResponseType() + { + if (isset($this->responseTypes['token'])) { + return $this->responseTypes['token']; + } + + if ($this->config['use_jwt_access_tokens']) { + return $this->createDefaultJwtAccessTokenResponseType(); + } + + return $this->createDefaultAccessTokenResponseType(); + } + + protected function getIdTokenResponseType() + { + if (isset($this->responseTypes['id_token'])) { + return $this->responseTypes['id_token']; + } + + return $this->createDefaultIdTokenResponseType(); + } + + protected function getIdTokenTokenResponseType() + { + if (isset($this->responseTypes['id_token token'])) { + return $this->responseTypes['id_token token']; + } + + return $this->createDefaultIdTokenTokenResponseType(); + } + + /** + * For Resource Controller + */ + protected function createDefaultJwtAccessTokenStorage() + { + if (!isset($this->storages['public_key'])) { + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens'); + } + $tokenStorage = null; + if (!empty($this->config['store_encrypted_token_string']) && isset($this->storages['access_token'])) { + $tokenStorage = $this->storages['access_token']; + } + // wrap the access token storage as required. + return new JwtAccessTokenStorage($this->storages['public_key'], $tokenStorage); + } + + /** + * For Authorize and Token Controllers + */ + protected function createDefaultJwtAccessTokenResponseType() + { + if (!isset($this->storages['public_key'])) { + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens'); + } + + $tokenStorage = null; + if (isset($this->storages['access_token'])) { + $tokenStorage = $this->storages['access_token']; + } + + $refreshStorage = null; + if (isset($this->storages['refresh_token'])) { + $refreshStorage = $this->storages['refresh_token']; + } + + $config = array_intersect_key($this->config, array_flip(explode(' ', 'store_encrypted_token_string issuer access_lifetime refresh_token_lifetime'))); + + return new JwtAccessToken($this->storages['public_key'], $tokenStorage, $refreshStorage, $config); + } + + protected function createDefaultAccessTokenResponseType() + { + if (!isset($this->storages['access_token'])) { + throw new \LogicException('You must supply a response type implementing OAuth2\ResponseType\AccessTokenInterface, or a storage object implementing OAuth2\Storage\AccessTokenInterface to use the token server'); + } + + $refreshStorage = null; + if (isset($this->storages['refresh_token'])) { + $refreshStorage = $this->storages['refresh_token']; + } + + $config = array_intersect_key($this->config, array_flip(explode(' ', 'access_lifetime refresh_token_lifetime'))); + $config['token_type'] = $this->tokenType ? $this->tokenType->getTokenType() : $this->getDefaultTokenType()->getTokenType(); + + return new AccessToken($this->storages['access_token'], $refreshStorage, $config); + } + + protected function createDefaultIdTokenResponseType() + { + if (!isset($this->storages['user_claims'])) { + throw new \LogicException('You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use openid connect'); + } + if (!isset($this->storages['public_key'])) { + throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use openid connect'); + } + + $config = array_intersect_key($this->config, array_flip(explode(' ', 'issuer id_lifetime'))); + + return new IdToken($this->storages['user_claims'], $this->storages['public_key'], $config); + } + + protected function createDefaultIdTokenTokenResponseType() + { + return new IdTokenToken($this->getAccessTokenResponseType(), $this->getIdTokenResponseType()); + } + + protected function validateOpenIdConnect() + { + $authCodeGrant = $this->getGrantType('authorization_code'); + if (!empty($authCodeGrant) && !$authCodeGrant instanceof OpenIDAuthorizationCodeGrantType) { + throw new \InvalidArgumentException('You have enabled OpenID Connect, but supplied a grant type that does not support it.'); + } + } + + protected function normalizeResponseType($name) + { + // for multiple-valued response types - make them alphabetical + if (!empty($name) && false !== strpos($name, ' ')) { + $types = explode(' ', $name); + sort($types); + $name = implode(' ', $types); + } + + return $name; + } + + public function getResponse() + { + return $this->response; + } + + public function getStorages() + { + return $this->storages; + } + + public function getStorage($name) + { + return isset($this->storages[$name]) ? $this->storages[$name] : null; + } + + public function getGrantTypes() + { + return $this->grantTypes; + } + + public function getGrantType($name) + { + return isset($this->grantTypes[$name]) ? $this->grantTypes[$name] : null; + } + + public function getResponseTypes() + { + return $this->responseTypes; + } + + public function getResponseType($name) + { + // for multiple-valued response types - make them alphabetical + $name = $this->normalizeResponseType($name); + + return isset($this->responseTypes[$name]) ? $this->responseTypes[$name] : null; + } + + public function getTokenType() + { + return $this->tokenType; + } + + public function getClientAssertionType() + { + return $this->clientAssertionType; + } + + public function setConfig($name, $value) + { + $this->config[$name] = $value; + } + + public function getConfig($name, $default = null) + { + return isset($this->config[$name]) ? $this->config[$name] : $default; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/AccessTokenInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/AccessTokenInterface.php new file mode 100644 index 000000000..1819158af --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/AccessTokenInterface.php @@ -0,0 +1,64 @@ +<?php + +namespace OAuth2\Storage; + +/** + * Implement this interface to specify where the OAuth2 Server + * should get/save access tokens + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +interface AccessTokenInterface +{ + /** + * Look up the supplied oauth_token from storage. + * + * We need to retrieve access token data as we create and verify tokens. + * + * @param $oauth_token + * oauth_token to be check with. + * + * @return + * An associative array as below, and return NULL if the supplied oauth_token + * is invalid: + * - expires: Stored expiration in unix timestamp. + * - client_id: (optional) Stored client identifier. + * - user_id: (optional) Stored user identifier. + * - scope: (optional) Stored scope values in space-separated string. + * - id_token: (optional) Stored id_token (if "use_openid_connect" is true). + * + * @ingroup oauth2_section_7 + */ + public function getAccessToken($oauth_token); + + /** + * Store the supplied access token values to storage. + * + * We need to store access token data as we create and verify tokens. + * + * @param $oauth_token oauth_token to be stored. + * @param $client_id client identifier to be stored. + * @param $user_id user identifier to be stored. + * @param int $expires expiration to be stored as a Unix timestamp. + * @param string $scope OPTIONAL Scopes to be stored in space-separated string. + * + * @ingroup oauth2_section_4 + */ + public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null); + + /** + * Expire an access token. + * + * This is not explicitly required in the spec, but if defined in a draft RFC for token + * revoking (RFC 7009) https://tools.ietf.org/html/rfc7009 + * + * @param $access_token + * Access token to be expired. + * + * @return BOOL true if an access token was unset, false if not + * @ingroup oauth2_section_6 + * + * @todo v2.0 include this method in interface. Omitted to maintain BC in v1.x + */ + //public function unsetAccessToken($access_token); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/AuthorizationCodeInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/AuthorizationCodeInterface.php new file mode 100644 index 000000000..edc7c70e5 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/AuthorizationCodeInterface.php @@ -0,0 +1,86 @@ +<?php + +namespace OAuth2\Storage; + +/** + * Implement this interface to specify where the OAuth2 Server + * should get/save authorization codes for the "Authorization Code" + * grant type + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +interface AuthorizationCodeInterface +{ + /** + * The Authorization Code grant type supports a response type of "code". + * + * @var string + * @see http://tools.ietf.org/html/rfc6749#section-1.4.1 + * @see http://tools.ietf.org/html/rfc6749#section-4.2 + */ + const RESPONSE_TYPE_CODE = "code"; + + /** + * Fetch authorization code data (probably the most common grant type). + * + * Retrieve the stored data for the given authorization code. + * + * Required for OAuth2::GRANT_TYPE_AUTH_CODE. + * + * @param $code + * Authorization code to be check with. + * + * @return + * An associative array as below, and NULL if the code is invalid + * @code + * return array( + * "client_id" => CLIENT_ID, // REQUIRED Stored client identifier + * "user_id" => USER_ID, // REQUIRED Stored user identifier + * "expires" => EXPIRES, // REQUIRED Stored expiration in unix timestamp + * "redirect_uri" => REDIRECT_URI, // REQUIRED Stored redirect URI + * "scope" => SCOPE, // OPTIONAL Stored scope values in space-separated string + * ); + * @endcode + * + * @see http://tools.ietf.org/html/rfc6749#section-4.1 + * + * @ingroup oauth2_section_4 + */ + public function getAuthorizationCode($code); + + /** + * Take the provided authorization code values and store them somewhere. + * + * This function should be the storage counterpart to getAuthCode(). + * + * If storage fails for some reason, we're not currently checking for + * any sort of success/failure, so you should bail out of the script + * and provide a descriptive fail message. + * + * Required for OAuth2::GRANT_TYPE_AUTH_CODE. + * + * @param string $code Authorization code to be stored. + * @param mixed $client_id Client identifier to be stored. + * @param mixed $user_id User identifier to be stored. + * @param string $redirect_uri Redirect URI(s) to be stored in a space-separated string. + * @param int $expires Expiration to be stored as a Unix timestamp. + * @param string $scope OPTIONAL Scopes to be stored in space-separated string. + * + * @ingroup oauth2_section_4 + */ + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null); + + /** + * once an Authorization Code is used, it must be expired + * + * @see http://tools.ietf.org/html/rfc6749#section-4.1.2 + * + * The client MUST NOT use the authorization code + * more than once. If an authorization code is used more than + * once, the authorization server MUST deny the request and SHOULD + * revoke (when possible) all tokens previously issued based on + * that authorization code + * + */ + public function expireAuthorizationCode($code); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Cassandra.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Cassandra.php new file mode 100644 index 000000000..c5048c08d --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Cassandra.php @@ -0,0 +1,480 @@ +<?php + +namespace OAuth2\Storage; + +use phpcassa\ColumnFamily; +use phpcassa\ColumnSlice; +use phpcassa\Connection\ConnectionPool; +use OAuth2\OpenID\Storage\UserClaimsInterface; +use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; + +/** + * Cassandra storage for all storage types + * + * To use, install "thobbs/phpcassa" via composer + * <code> + * composer require thobbs/phpcassa:dev-master + * </code> + * + * Once this is done, instantiate the + * <code> + * $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_server', array('127.0.0.1:9160')); + * </code> + * + * Then, register the storage client: + * <code> + * $storage = new OAuth2\Storage\Cassandra($cassandra); + * $storage->setClientDetails($client_id, $client_secret, $redirect_uri); + * </code> + * + * @see test/lib/OAuth2/Storage/Bootstrap::getCassandraStorage + */ +class Cassandra implements AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + ScopeInterface, + PublicKeyInterface, + UserClaimsInterface, + OpenIDAuthorizationCodeInterface +{ + + private $cache; + + /* The cassandra client */ + protected $cassandra; + + /* Configuration array */ + protected $config; + + /** + * Cassandra Storage! uses phpCassa + * + * @param \phpcassa\ConnectionPool $cassandra + * @param array $config + */ + public function __construct($connection = array(), array $config = array()) + { + if ($connection instanceof ConnectionPool) { + $this->cassandra = $connection; + } else { + if (!is_array($connection)) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Cassandra must be an instance of phpcassa\Connection\ConnectionPool or a configuration array'); + } + $connection = array_merge(array( + 'keyspace' => 'oauth2', + 'servers' => null, + ), $connection); + + $this->cassandra = new ConnectionPool($connection['keyspace'], $connection['servers']); + } + + $this->config = array_merge(array( + // cassandra config + 'column_family' => 'auth', + + // key names + 'client_key' => 'oauth_clients:', + 'access_token_key' => 'oauth_access_tokens:', + 'refresh_token_key' => 'oauth_refresh_tokens:', + 'code_key' => 'oauth_authorization_codes:', + 'user_key' => 'oauth_users:', + 'jwt_key' => 'oauth_jwt:', + 'scope_key' => 'oauth_scopes:', + 'public_key_key' => 'oauth_public_keys:', + ), $config); + } + + protected function getValue($key) + { + if (isset($this->cache[$key])) { + return $this->cache[$key]; + } + $cf = new ColumnFamily($this->cassandra, $this->config['column_family']); + + try { + $value = $cf->get($key, new ColumnSlice("", "")); + $value = array_shift($value); + } catch (\cassandra\NotFoundException $e) { + return false; + } + + return json_decode($value, true); + } + + protected function setValue($key, $value, $expire = 0) + { + $this->cache[$key] = $value; + + $cf = new ColumnFamily($this->cassandra, $this->config['column_family']); + + $str = json_encode($value); + if ($expire > 0) { + try { + $seconds = $expire - time(); + // __data key set as C* requires a field, note: max TTL can only be 630720000 seconds + $cf->insert($key, array('__data' => $str), null, $seconds); + } catch (\Exception $e) { + return false; + } + } else { + try { + // __data key set as C* requires a field + $cf->insert($key, array('__data' => $str)); + } catch (\Exception $e) { + return false; + } + } + + return true; + } + + protected function expireValue($key) + { + unset($this->cache[$key]); + + $cf = new ColumnFamily($this->cassandra, $this->config['column_family']); + + if ($cf->get_count($key) > 0) { + try { + // __data key set as C* requires a field + $cf->remove($key, array('__data')); + } catch (\Exception $e) { + return false; + } + + return true; + } + + return false; + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + return $this->getValue($this->config['code_key'] . $code); + } + + public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + return $this->setValue( + $this->config['code_key'] . $authorization_code, + compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'), + $expires + ); + } + + public function expireAuthorizationCode($code) + { + $key = $this->config['code_key'] . $code; + unset($this->cache[$key]); + + return $this->expireValue($key); + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + + return false; + } + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == $this->hashPassword($password); + } + + // use a secure hashing algorithm when storing passwords. Override this for your application + protected function hashPassword($password) + { + return sha1($password); + } + + public function getUserDetails($username) + { + return $this->getUser($username); + } + + public function getUser($username) + { + if (!$userInfo = $this->getValue($this->config['user_key'] . $username)) { + return false; + } + + // the default behavior is to use "username" as the user_id + return array_merge(array( + 'user_id' => $username, + ), $userInfo); + } + + public function setUser($username, $password, $first_name = null, $last_name = null) + { + $password = $this->hashPassword($password); + + return $this->setValue( + $this->config['user_key'] . $username, + compact('username', 'password', 'first_name', 'last_name') + ); + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if (!$client = $this->getClientDetails($client_id)) { + return false; + } + + return isset($client['client_secret']) + && $client['client_secret'] == $client_secret; + } + + public function isPublicClient($client_id) + { + if (!$client = $this->getClientDetails($client_id)) { + return false; + } + + return empty($client['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + return $this->getValue($this->config['client_key'] . $client_id); + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + return $this->setValue( + $this->config['client_key'] . $client_id, + compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id') + ); + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, (array) $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + return $this->getValue($this->config['refresh_token_key'] . $refresh_token); + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + return $this->setValue( + $this->config['refresh_token_key'] . $refresh_token, + compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'), + $expires + ); + } + + public function unsetRefreshToken($refresh_token) + { + return $this->expireValue($this->config['refresh_token_key'] . $refresh_token); + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + return $this->getValue($this->config['access_token_key'].$access_token); + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + return $this->setValue( + $this->config['access_token_key'].$access_token, + compact('access_token', 'client_id', 'user_id', 'expires', 'scope'), + $expires + ); + } + + public function unsetAccessToken($access_token) + { + return $this->expireValue($this->config['access_token_key'] . $access_token); + } + + /* ScopeInterface */ + public function scopeExists($scope) + { + $scope = explode(' ', $scope); + + $result = $this->getValue($this->config['scope_key'].'supported:global'); + + $supportedScope = explode(' ', (string) $result); + + return (count(array_diff($scope, $supportedScope)) == 0); + } + + public function getDefaultScope($client_id = null) + { + if (is_null($client_id) || !$result = $this->getValue($this->config['scope_key'].'default:'.$client_id)) { + $result = $this->getValue($this->config['scope_key'].'default:global'); + } + + return $result; + } + + public function setScope($scope, $client_id = null, $type = 'supported') + { + if (!in_array($type, array('default', 'supported'))) { + throw new \InvalidArgumentException('"$type" must be one of "default", "supported"'); + } + + if (is_null($client_id)) { + $key = $this->config['scope_key'].$type.':global'; + } else { + $key = $this->config['scope_key'].$type.':'.$client_id; + } + + return $this->setValue($key, $scope); + } + + /*JWTBearerInterface */ + public function getClientKey($client_id, $subject) + { + if (!$jwt = $this->getValue($this->config['jwt_key'] . $client_id)) { + return false; + } + + if (isset($jwt['subject']) && $jwt['subject'] == $subject ) { + return $jwt['key']; + } + + return null; + } + + public function setClientKey($client_id, $key, $subject = null) + { + return $this->setValue($this->config['jwt_key'] . $client_id, array( + 'key' => $key, + 'subject' => $subject + )); + } + + /*ScopeInterface */ + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs cassandra implementation. + throw new \Exception('getJti() for the Cassandra driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs cassandra implementation. + throw new \Exception('setJti() for the Cassandra driver is currently unimplemented.'); + } + + /* PublicKeyInterface */ + public function getPublicKey($client_id = '') + { + $public_key = $this->getValue($this->config['public_key_key'] . $client_id); + if (is_array($public_key)) { + return $public_key['public_key']; + } + $public_key = $this->getValue($this->config['public_key_key']); + if (is_array($public_key)) { + return $public_key['public_key']; + } + } + + public function getPrivateKey($client_id = '') + { + $public_key = $this->getValue($this->config['public_key_key'] . $client_id); + if (is_array($public_key)) { + return $public_key['private_key']; + } + $public_key = $this->getValue($this->config['public_key_key']); + if (is_array($public_key)) { + return $public_key['private_key']; + } + } + + public function getEncryptionAlgorithm($client_id = null) + { + $public_key = $this->getValue($this->config['public_key_key'] . $client_id); + if (is_array($public_key)) { + return $public_key['encryption_algorithm']; + } + $public_key = $this->getValue($this->config['public_key_key']); + if (is_array($public_key)) { + return $public_key['encryption_algorithm']; + } + + return 'RS256'; + } + + /* UserClaimsInterface */ + public function getUserClaims($user_id, $claims) + { + $userDetails = $this->getUserDetails($user_id); + if (!is_array($userDetails)) { + return false; + } + + $claims = explode(' ', trim($claims)); + $userClaims = array(); + + // for each requested claim, if the user has the claim, set it in the response + $validClaims = explode(' ', self::VALID_CLAIMS); + foreach ($validClaims as $validClaim) { + if (in_array($validClaim, $claims)) { + if ($validClaim == 'address') { + // address is an object with subfields + $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); + } else { + $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails)); + } + } + } + + return $userClaims; + } + + protected function getUserClaim($claim, $userDetails) + { + $userClaims = array(); + $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim))); + $claimValues = explode(' ', $claimValuesString); + + foreach ($claimValues as $value) { + if ($value == 'email_verified') { + $userClaims[$value] = $userDetails[$value]=='true' ? true : false; + } else { + $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null; + } + } + + return $userClaims; + } + +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientCredentialsInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientCredentialsInterface.php new file mode 100644 index 000000000..3318c6966 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientCredentialsInterface.php @@ -0,0 +1,49 @@ +<?php + +namespace OAuth2\Storage; + +/** + * Implement this interface to specify how the OAuth2 Server + * should verify client credentials + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +interface ClientCredentialsInterface extends ClientInterface +{ + + /** + * Make sure that the client credentials is valid. + * + * @param $client_id + * Client identifier to be check with. + * @param $client_secret + * (optional) If a secret is required, check that they've given the right one. + * + * @return + * TRUE if the client credentials are valid, and MUST return FALSE if it isn't. + * @endcode + * + * @see http://tools.ietf.org/html/rfc6749#section-3.1 + * + * @ingroup oauth2_section_3 + */ + public function checkClientCredentials($client_id, $client_secret = null); + + /** + * Determine if the client is a "public" client, and therefore + * does not require passing credentials for certain grant types + * + * @param $client_id + * Client identifier to be check with. + * + * @return + * TRUE if the client is public, and FALSE if it isn't. + * @endcode + * + * @see http://tools.ietf.org/html/rfc6749#section-2.3 + * @see https://github.com/bshaffer/oauth2-server-php/issues/257 + * + * @ingroup oauth2_section_2 + */ + public function isPublicClient($client_id); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientInterface.php new file mode 100644 index 000000000..09a5bffc1 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientInterface.php @@ -0,0 +1,66 @@ +<?php + +namespace OAuth2\Storage; + +/** + * Implement this interface to specify where the OAuth2 Server + * should retrieve client information + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +interface ClientInterface +{ + /** + * Get client details corresponding client_id. + * + * OAuth says we should store request URIs for each registered client. + * Implement this function to grab the stored URI for a given client id. + * + * @param $client_id + * Client identifier to be check with. + * + * @return array + * Client details. The only mandatory key in the array is "redirect_uri". + * This function MUST return FALSE if the given client does not exist or is + * invalid. "redirect_uri" can be space-delimited to allow for multiple valid uris. + * <code> + * return array( + * "redirect_uri" => REDIRECT_URI, // REQUIRED redirect_uri registered for the client + * "client_id" => CLIENT_ID, // OPTIONAL the client id + * "grant_types" => GRANT_TYPES, // OPTIONAL an array of restricted grant types + * "user_id" => USER_ID, // OPTIONAL the user identifier associated with this client + * "scope" => SCOPE, // OPTIONAL the scopes allowed for this client + * ); + * </code> + * + * @ingroup oauth2_section_4 + */ + public function getClientDetails($client_id); + + /** + * Get the scope associated with this client + * + * @return + * STRING the space-delineated scope list for the specified client_id + */ + public function getClientScope($client_id); + + /** + * Check restricted grant types of corresponding client identifier. + * + * If you want to restrict clients to certain grant types, override this + * function. + * + * @param $client_id + * Client identifier to be check with. + * @param $grant_type + * Grant type to be check with + * + * @return + * TRUE if the grant type is supported by this client identifier, and + * FALSE if it isn't. + * + * @ingroup oauth2_section_4 + */ + public function checkRestrictedGrantType($client_id, $grant_type); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/CouchbaseDB.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/CouchbaseDB.php new file mode 100755 index 000000000..1eb55f027 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/CouchbaseDB.php @@ -0,0 +1,331 @@ +<?php + +namespace OAuth2\Storage; + +use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; + +/** + * Simple Couchbase storage for all storage types + * + * This class should be extended or overridden as required + * + * NOTE: Passwords are stored in plaintext, which is never + * a good idea. Be sure to override this for your application + * + * @author Tom Park <tom@raucter.com> + */ +class CouchbaseDB implements AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + OpenIDAuthorizationCodeInterface +{ + protected $db; + protected $config; + + public function __construct($connection, $config = array()) + { + if ($connection instanceof \Couchbase) { + $this->db = $connection; + } else { + if (!is_array($connection) || !is_array($connection['servers'])) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\CouchbaseDB must be an instance of Couchbase or a configuration array containing a server array'); + } + + $this->db = new \Couchbase($connection['servers'], (!isset($connection['username'])) ? '' : $connection['username'], (!isset($connection['password'])) ? '' : $connection['password'], $connection['bucket'], false); + } + + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'jwt_table' => 'oauth_jwt', + ), $config); + } + + // Helper function to access couchbase item by type: + protected function getObjectByType($name,$id) + { + return json_decode($this->db->get($this->config[$name].'-'.$id),true); + } + + // Helper function to set couchbase item by type: + protected function setObjectByType($name,$id,$array) + { + $array['type'] = $name; + + return $this->db->set($this->config[$name].'-'.$id,json_encode($array)); + } + + // Helper function to delete couchbase item by type, wait for persist to at least 1 node + protected function deleteObjectByType($name,$id) + { + $this->db->delete($this->config[$name].'-'.$id,"",1); + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if ($result = $this->getObjectByType('client_table',$client_id)) { + return $result['client_secret'] == $client_secret; + } + + return false; + } + + public function isPublicClient($client_id) + { + if (!$result = $this->getObjectByType('client_table',$client_id)) { + return false; + } + + return empty($result['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + $result = $this->getObjectByType('client_table',$client_id); + + return is_null($result) ? false : $result; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + if ($this->getClientDetails($client_id)) { + + $this->setObjectByType('client_table',$client_id, array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + )); + } else { + $this->setObjectByType('client_table',$client_id, array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + )); + } + + return true; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + $token = $this->getObjectByType('access_token_table',$access_token); + + return is_null($token) ? false : $token; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // if it exists, update it. + if ($this->getAccessToken($access_token)) { + $this->setObjectByType('access_token_table',$access_token, array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + )); + } else { + $this->setObjectByType('access_token_table',$access_token, array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + )); + } + + return true; + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + $code = $this->getObjectByType('code_table',$code); + + return is_null($code) ? false : $code; + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $this->setObjectByType('code_table',$code, array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + )); + } else { + $this->setObjectByType('code_table',$code,array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + )); + } + + return true; + } + + public function expireAuthorizationCode($code) + { + $this->deleteObjectByType('code_table',$code); + + return true; + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + + return false; + } + + public function getUserDetails($username) + { + if ($user = $this->getUser($username)) { + $user['user_id'] = $user['username']; + } + + return $user; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + $token = $this->getObjectByType('refresh_token_table',$refresh_token); + + return is_null($token) ? false : $token; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + $this->setObjectByType('refresh_token_table',$refresh_token, array( + 'refresh_token' => $refresh_token, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope + )); + + return true; + } + + public function unsetRefreshToken($refresh_token) + { + $this->deleteObjectByType('refresh_token_table',$refresh_token); + + return true; + } + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == $password; + } + + public function getUser($username) + { + $result = $this->getObjectByType('user_table',$username); + + return is_null($result) ? false : $result; + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + if ($this->getUser($username)) { + $this->setObjectByType('user_table',$username, array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + )); + + } else { + $this->setObjectByType('user_table',$username, array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + )); + + } + + return true; + } + + public function getClientKey($client_id, $subject) + { + if (!$jwt = $this->getObjectByType('jwt_table',$client_id)) { + return false; + } + + if (isset($jwt['subject']) && $jwt['subject'] == $subject) { + return $jwt['key']; + } + + return false; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs couchbase implementation. + throw new \Exception('getJti() for the Couchbase driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs couchbase implementation. + throw new \Exception('setJti() for the Couchbase driver is currently unimplemented.'); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/DynamoDB.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/DynamoDB.php new file mode 100644 index 000000000..8347ab258 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/DynamoDB.php @@ -0,0 +1,540 @@ +<?php + +namespace OAuth2\Storage; + +use Aws\DynamoDb\DynamoDbClient; + +use OAuth2\OpenID\Storage\UserClaimsInterface; +use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; +/** + * DynamoDB storage for all storage types + * + * To use, install "aws/aws-sdk-php" via composer + * <code> + * composer require aws/aws-sdk-php:dev-master + * </code> + * + * Once this is done, instantiate the DynamoDB client + * <code> + * $storage = new OAuth2\Storage\Dynamodb(array("key" => "YOURKEY", "secret" => "YOURSECRET", "region" => "YOURREGION")); + * </code> + * + * Table : + * - oauth_access_tokens (primary hash key : access_token) + * - oauth_authorization_codes (primary hash key : authorization_code) + * - oauth_clients (primary hash key : client_id) + * - oauth_jwt (primary hash key : client_id, primary range key : subject) + * - oauth_public_keys (primary hash key : client_id) + * - oauth_refresh_tokens (primary hash key : refresh_token) + * - oauth_scopes (primary hash key : scope, secondary index : is_default-index hash key is_default) + * - oauth_users (primary hash key : username) + * + * @author Frederic AUGUSTE <frederic.auguste at gmail dot com> + */ +class DynamoDB implements + AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + ScopeInterface, + PublicKeyInterface, + UserClaimsInterface, + OpenIDAuthorizationCodeInterface +{ + protected $client; + protected $config; + + public function __construct($connection, $config = array()) + { + if (!($connection instanceof DynamoDbClient)) { + if (!is_array($connection)) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Dynamodb must be an instance a configuration array containt key, secret, region'); + } + if (!array_key_exists("key",$connection) || !array_key_exists("secret",$connection) || !array_key_exists("region",$connection) ) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Dynamodb must be an instance a configuration array containt key, secret, region'); + } + $this->client = DynamoDbClient::factory(array( + 'key' => $connection["key"], + 'secret' => $connection["secret"], + 'region' =>$connection["region"] + )); + } else { + $this->client = $connection; + } + + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'jwt_table' => 'oauth_jwt', + 'scope_table' => 'oauth_scopes', + 'public_key_table' => 'oauth_public_keys', + ), $config); + } + + /* OAuth2\Storage\ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['client_table'], + "Key" => array('client_id' => array('S' => $client_id)) + )); + + return $result->count()==1 && $result["Item"]["client_secret"]["S"] == $client_secret; + } + + public function isPublicClient($client_id) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['client_table'], + "Key" => array('client_id' => array('S' => $client_id)) + )); + + if ($result->count()==0) { + return false ; + } + + return empty($result["Item"]["client_secret"]); + } + + /* OAuth2\Storage\ClientInterface */ + public function getClientDetails($client_id) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['client_table'], + "Key" => array('client_id' => array('S' => $client_id)) + )); + if ($result->count()==0) { + return false ; + } + $result = $this->dynamo2array($result); + foreach (array('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id') as $key => $val) { + if (!array_key_exists ($val, $result)) { + $result[$val] = null; + } + } + + return $result; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + $clientData = compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id'); + $clientData = array_filter($clientData, 'self::isNotEmpty'); + + $result = $this->client->putItem(array( + 'TableName' => $this->config['client_table'], + 'Item' => $this->client->formatAttributes($clientData) + )); + + return true; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, (array) $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* OAuth2\Storage\AccessTokenInterface */ + public function getAccessToken($access_token) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['access_token_table'], + "Key" => array('access_token' => array('S' => $access_token)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + if (array_key_exists ('expires', $token)) { + $token['expires'] = strtotime($token['expires']); + } + + return $token; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + $clientData = compact('access_token', 'client_id', 'user_id', 'expires', 'scope'); + $clientData = array_filter($clientData, 'self::isNotEmpty'); + + $result = $this->client->putItem(array( + 'TableName' => $this->config['access_token_table'], + 'Item' => $this->client->formatAttributes($clientData) + )); + + return true; + + } + + public function unsetAccessToken($access_token) + { + $result = $this->client->deleteItem(array( + 'TableName' => $this->config['access_token_table'], + 'Key' => $this->client->formatAttributes(array("access_token" => $access_token)), + 'ReturnValues' => 'ALL_OLD', + )); + + return null !== $result->get('Attributes'); + } + + /* OAuth2\Storage\AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['code_table'], + "Key" => array('authorization_code' => array('S' => $code)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + if (!array_key_exists("id_token", $token )) { + $token['id_token'] = null; + } + $token['expires'] = strtotime($token['expires']); + + return $token; + + } + + public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + $clientData = compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'id_token', 'scope'); + $clientData = array_filter($clientData, 'self::isNotEmpty'); + + $result = $this->client->putItem(array( + 'TableName' => $this->config['code_table'], + 'Item' => $this->client->formatAttributes($clientData) + )); + + return true; + } + + public function expireAuthorizationCode($code) + { + + $result = $this->client->deleteItem(array( + 'TableName' => $this->config['code_table'], + 'Key' => $this->client->formatAttributes(array("authorization_code" => $code)) + )); + + return true; + } + + /* OAuth2\Storage\UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + + return false; + } + + public function getUserDetails($username) + { + return $this->getUser($username); + } + + /* UserClaimsInterface */ + public function getUserClaims($user_id, $claims) + { + if (!$userDetails = $this->getUserDetails($user_id)) { + return false; + } + + $claims = explode(' ', trim($claims)); + $userClaims = array(); + + // for each requested claim, if the user has the claim, set it in the response + $validClaims = explode(' ', self::VALID_CLAIMS); + foreach ($validClaims as $validClaim) { + if (in_array($validClaim, $claims)) { + if ($validClaim == 'address') { + // address is an object with subfields + $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); + } else { + $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails)); + } + } + } + + return $userClaims; + } + + protected function getUserClaim($claim, $userDetails) + { + $userClaims = array(); + $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim))); + $claimValues = explode(' ', $claimValuesString); + + foreach ($claimValues as $value) { + if ($value == 'email_verified') { + $userClaims[$value] = $userDetails[$value]=='true' ? true : false; + } else { + $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null; + } + } + + return $userClaims; + } + + /* OAuth2\Storage\RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['refresh_token_table'], + "Key" => array('refresh_token' => array('S' => $refresh_token)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + $token['expires'] = strtotime($token['expires']); + + return $token; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + $clientData = compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'); + $clientData = array_filter($clientData, 'self::isNotEmpty'); + + $result = $this->client->putItem(array( + 'TableName' => $this->config['refresh_token_table'], + 'Item' => $this->client->formatAttributes($clientData) + )); + + return true; + } + + public function unsetRefreshToken($refresh_token) + { + $result = $this->client->deleteItem(array( + 'TableName' => $this->config['refresh_token_table'], + 'Key' => $this->client->formatAttributes(array("refresh_token" => $refresh_token)) + )); + + return true; + } + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == $this->hashPassword($password); + } + + // use a secure hashing algorithm when storing passwords. Override this for your application + protected function hashPassword($password) + { + return sha1($password); + } + + public function getUser($username) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['user_table'], + "Key" => array('username' => array('S' => $username)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + $token['user_id'] = $username; + + return $token; + } + + public function setUser($username, $password, $first_name = null, $last_name = null) + { + // do not store in plaintext + $password = $this->hashPassword($password); + + $clientData = compact('username', 'password', 'first_name', 'last_name'); + $clientData = array_filter($clientData, 'self::isNotEmpty'); + + $result = $this->client->putItem(array( + 'TableName' => $this->config['user_table'], + 'Item' => $this->client->formatAttributes($clientData) + )); + + return true; + + } + + /* ScopeInterface */ + public function scopeExists($scope) + { + $scope = explode(' ', $scope); + $scope_query = array(); + $count = 0; + foreach ($scope as $key => $val) { + $result = $this->client->query(array( + 'TableName' => $this->config['scope_table'], + 'Select' => 'COUNT', + 'KeyConditions' => array( + 'scope' => array( + 'AttributeValueList' => array(array('S' => $val)), + 'ComparisonOperator' => 'EQ' + ) + ) + )); + $count += $result['Count']; + } + + return $count == count($scope); + } + + public function getDefaultScope($client_id = null) + { + + $result = $this->client->query(array( + 'TableName' => $this->config['scope_table'], + 'IndexName' => 'is_default-index', + 'Select' => 'ALL_ATTRIBUTES', + 'KeyConditions' => array( + 'is_default' => array( + 'AttributeValueList' => array(array('S' => 'true')), + 'ComparisonOperator' => 'EQ', + ), + ) + )); + $defaultScope = array(); + if ($result->count() > 0) { + $array = $result->toArray(); + foreach ($array["Items"] as $item) { + $defaultScope[] = $item['scope']['S']; + } + + return empty($defaultScope) ? null : implode(' ', $defaultScope); + } + + return null; + } + + /* JWTBearerInterface */ + public function getClientKey($client_id, $subject) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['jwt_table'], + "Key" => array('client_id' => array('S' => $client_id), 'subject' => array('S' => $subject)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + + return $token['public_key']; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expires, $jti) + { + //TODO not use. + } + + public function setJti($client_id, $subject, $audience, $expires, $jti) + { + //TODO not use. + } + + /* PublicKeyInterface */ + public function getPublicKey($client_id = '0') + { + + $result = $this->client->getItem(array( + "TableName"=> $this->config['public_key_table'], + "Key" => array('client_id' => array('S' => $client_id)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + + return $token['public_key']; + + } + + public function getPrivateKey($client_id = '0') + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['public_key_table'], + "Key" => array('client_id' => array('S' => $client_id)) + )); + if ($result->count()==0) { + return false ; + } + $token = $this->dynamo2array($result); + + return $token['private_key']; + } + + public function getEncryptionAlgorithm($client_id = null) + { + $result = $this->client->getItem(array( + "TableName"=> $this->config['public_key_table'], + "Key" => array('client_id' => array('S' => $client_id)) + )); + if ($result->count()==0) { + return 'RS256' ; + } + $token = $this->dynamo2array($result); + + return $token['encryption_algorithm']; + } + + /** + * Transform dynamodb resultset to an array. + * @param $dynamodbResult + * @return $array + */ + private function dynamo2array($dynamodbResult) + { + $result = array(); + foreach ($dynamodbResult["Item"] as $key => $val) { + $result[$key] = $val["S"]; + $result[] = $val["S"]; + } + + return $result; + } + + private static function isNotEmpty($value) + { + return null !== $value && '' !== $value; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessToken.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessToken.php new file mode 100644 index 000000000..75b49d301 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessToken.php @@ -0,0 +1,88 @@ +<?php + +namespace OAuth2\Storage; + +use OAuth2\Encryption\EncryptionInterface; +use OAuth2\Encryption\Jwt; + +/** + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +class JwtAccessToken implements JwtAccessTokenInterface +{ + protected $publicKeyStorage; + protected $tokenStorage; + protected $encryptionUtil; + + /** + * @param OAuth2\Encryption\PublicKeyInterface $publicKeyStorage the public key encryption to use + * @param OAuth2\Storage\AccessTokenInterface $tokenStorage OPTIONAL persist the access token to another storage. This is useful if + * you want to retain access token grant information somewhere, but + * is not necessary when using this grant type. + * @param OAuth2\Encryption\EncryptionInterface $encryptionUtil OPTIONAL class to use for "encode" and "decode" functions. + */ + public function __construct(PublicKeyInterface $publicKeyStorage, AccessTokenInterface $tokenStorage = null, EncryptionInterface $encryptionUtil = null) + { + $this->publicKeyStorage = $publicKeyStorage; + $this->tokenStorage = $tokenStorage; + if (is_null($encryptionUtil)) { + $encryptionUtil = new Jwt; + } + $this->encryptionUtil = $encryptionUtil; + } + + public function getAccessToken($oauth_token) + { + // just decode the token, don't verify + if (!$tokenData = $this->encryptionUtil->decode($oauth_token, null, false)) { + return false; + } + + $client_id = isset($tokenData['aud']) ? $tokenData['aud'] : null; + $public_key = $this->publicKeyStorage->getPublicKey($client_id); + $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); + + // now that we have the client_id, verify the token + if (false === $this->encryptionUtil->decode($oauth_token, $public_key, array($algorithm))) { + return false; + } + + // normalize the JWT claims to the format expected by other components in this library + return $this->convertJwtToOAuth2($tokenData); + } + + public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null) + { + if ($this->tokenStorage) { + return $this->tokenStorage->setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope); + } + } + + public function unsetAccessToken($access_token) + { + if ($this->tokenStorage) { + return $this->tokenStorage->unsetAccessToken($access_token); + } + } + + + // converts a JWT access token into an OAuth2-friendly format + protected function convertJwtToOAuth2($tokenData) + { + $keyMapping = array( + 'aud' => 'client_id', + 'exp' => 'expires', + 'sub' => 'user_id' + ); + + foreach ($keyMapping as $jwtKey => $oauth2Key) { + if (isset($tokenData[$jwtKey])) { + $tokenData[$oauth2Key] = $tokenData[$jwtKey]; + unset($tokenData[$jwtKey]); + } + } + + return $tokenData; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessTokenInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessTokenInterface.php new file mode 100644 index 000000000..3abb2aa2d --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessTokenInterface.php @@ -0,0 +1,14 @@ +<?php + +namespace OAuth2\Storage; + +/** + * No specific methods, but allows the library to check "instanceof" + * against interface rather than class + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +interface JwtAccessTokenInterface extends AccessTokenInterface +{ + +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtBearerInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtBearerInterface.php new file mode 100644 index 000000000..c83aa72ea --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtBearerInterface.php @@ -0,0 +1,74 @@ +<?php + +namespace OAuth2\Storage; + +/** + * Implement this interface to specify where the OAuth2 Server + * should get the JWT key for clients + * + * @TODO consider extending ClientInterface, as this will almost always + * be the same storage as retrieving clientData + * + * @author F21 + * @author Brent Shaffer <bshafs at gmail dot com> + */ +interface JwtBearerInterface +{ + /** + * Get the public key associated with a client_id + * + * @param $client_id + * Client identifier to be checked with. + * + * @return + * STRING Return the public key for the client_id if it exists, and MUST return FALSE if it doesn't. + */ + public function getClientKey($client_id, $subject); + + /** + * Get a jti (JSON token identifier) by matching against the client_id, subject, audience and expiration. + * + * @param $client_id + * Client identifier to match. + * + * @param $subject + * The subject to match. + * + * @param $audience + * The audience to match. + * + * @param $expiration + * The expiration of the jti. + * + * @param $jti + * The jti to match. + * + * @return + * An associative array as below, and return NULL if the jti does not exist. + * - issuer: Stored client identifier. + * - subject: Stored subject. + * - audience: Stored audience. + * - expires: Stored expiration in unix timestamp. + * - jti: The stored jti. + */ + public function getJti($client_id, $subject, $audience, $expiration, $jti); + + /** + * Store a used jti so that we can check against it to prevent replay attacks. + * @param $client_id + * Client identifier to insert. + * + * @param $subject + * The subject to insert. + * + * @param $audience + * The audience to insert. + * + * @param $expiration + * The expiration of the jti. + * + * @param $jti + * The jti to insert. + */ + public function setJti($client_id, $subject, $audience, $expiration, $jti); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Memory.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Memory.php new file mode 100644 index 000000000..42d833ccb --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Memory.php @@ -0,0 +1,381 @@ +<?php + +namespace OAuth2\Storage; + +use OAuth2\OpenID\Storage\UserClaimsInterface; +use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; + +/** + * Simple in-memory storage for all storage types + * + * NOTE: This class should never be used in production, and is + * a stub class for example use only + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +class Memory implements AuthorizationCodeInterface, + UserCredentialsInterface, + UserClaimsInterface, + AccessTokenInterface, + ClientCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + ScopeInterface, + PublicKeyInterface, + OpenIDAuthorizationCodeInterface +{ + public $authorizationCodes; + public $userCredentials; + public $clientCredentials; + public $refreshTokens; + public $accessTokens; + public $jwt; + public $jti; + public $supportedScopes; + public $defaultScope; + public $keys; + + public function __construct($params = array()) + { + $params = array_merge(array( + 'authorization_codes' => array(), + 'user_credentials' => array(), + 'client_credentials' => array(), + 'refresh_tokens' => array(), + 'access_tokens' => array(), + 'jwt' => array(), + 'jti' => array(), + 'default_scope' => null, + 'supported_scopes' => array(), + 'keys' => array(), + ), $params); + + $this->authorizationCodes = $params['authorization_codes']; + $this->userCredentials = $params['user_credentials']; + $this->clientCredentials = $params['client_credentials']; + $this->refreshTokens = $params['refresh_tokens']; + $this->accessTokens = $params['access_tokens']; + $this->jwt = $params['jwt']; + $this->jti = $params['jti']; + $this->supportedScopes = $params['supported_scopes']; + $this->defaultScope = $params['default_scope']; + $this->keys = $params['keys']; + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + if (!isset($this->authorizationCodes[$code])) { + return false; + } + + return array_merge(array( + 'authorization_code' => $code, + ), $this->authorizationCodes[$code]); + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + $this->authorizationCodes[$code] = compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'); + + return true; + } + + public function setAuthorizationCodes($authorization_codes) + { + $this->authorizationCodes = $authorization_codes; + } + + public function expireAuthorizationCode($code) + { + unset($this->authorizationCodes[$code]); + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + $userDetails = $this->getUserDetails($username); + + return $userDetails && $userDetails['password'] && $userDetails['password'] === $password; + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + $this->userCredentials[$username] = array( + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName, + ); + + return true; + } + + public function getUserDetails($username) + { + if (!isset($this->userCredentials[$username])) { + return false; + } + + return array_merge(array( + 'user_id' => $username, + 'password' => null, + 'first_name' => null, + 'last_name' => null, + ), $this->userCredentials[$username]); + } + + /* UserClaimsInterface */ + public function getUserClaims($user_id, $claims) + { + if (!$userDetails = $this->getUserDetails($user_id)) { + return false; + } + + $claims = explode(' ', trim($claims)); + $userClaims = array(); + + // for each requested claim, if the user has the claim, set it in the response + $validClaims = explode(' ', self::VALID_CLAIMS); + foreach ($validClaims as $validClaim) { + if (in_array($validClaim, $claims)) { + if ($validClaim == 'address') { + // address is an object with subfields + $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); + } else { + $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails)); + } + } + } + + return $userClaims; + } + + protected function getUserClaim($claim, $userDetails) + { + $userClaims = array(); + $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim))); + $claimValues = explode(' ', $claimValuesString); + + foreach ($claimValues as $value) { + $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null; + } + + return $userClaims; + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + return isset($this->clientCredentials[$client_id]['client_secret']) && $this->clientCredentials[$client_id]['client_secret'] === $client_secret; + } + + public function isPublicClient($client_id) + { + if (!isset($this->clientCredentials[$client_id])) { + return false; + } + + return empty($this->clientCredentials[$client_id]['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + if (!isset($this->clientCredentials[$client_id])) { + return false; + } + + $clientDetails = array_merge(array( + 'client_id' => $client_id, + 'client_secret' => null, + 'redirect_uri' => null, + 'scope' => null, + ), $this->clientCredentials[$client_id]); + + return $clientDetails; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + if (isset($this->clientCredentials[$client_id]['grant_types'])) { + $grant_types = explode(' ', $this->clientCredentials[$client_id]['grant_types']); + + return in_array($grant_type, $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + $this->clientCredentials[$client_id] = array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + ); + + return true; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + return isset($this->refreshTokens[$refresh_token]) ? $this->refreshTokens[$refresh_token] : false; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + $this->refreshTokens[$refresh_token] = compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'); + + return true; + } + + public function unsetRefreshToken($refresh_token) + { + if (isset($this->refreshTokens[$refresh_token])) { + unset($this->refreshTokens[$refresh_token]); + + return true; + } + + return false; + } + + public function setRefreshTokens($refresh_tokens) + { + $this->refreshTokens = $refresh_tokens; + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + return isset($this->accessTokens[$access_token]) ? $this->accessTokens[$access_token] : false; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null, $id_token = null) + { + $this->accessTokens[$access_token] = compact('access_token', 'client_id', 'user_id', 'expires', 'scope', 'id_token'); + + return true; + } + + public function unsetAccessToken($access_token) + { + if (isset($this->accessTokens[$access_token])) { + unset($this->accessTokens[$access_token]); + + return true; + } + + return false; + } + + public function scopeExists($scope) + { + $scope = explode(' ', trim($scope)); + + return (count(array_diff($scope, $this->supportedScopes)) == 0); + } + + public function getDefaultScope($client_id = null) + { + return $this->defaultScope; + } + + /*JWTBearerInterface */ + public function getClientKey($client_id, $subject) + { + if (isset($this->jwt[$client_id])) { + $jwt = $this->jwt[$client_id]; + if ($jwt) { + if ($jwt["subject"] == $subject) { + return $jwt["key"]; + } + } + } + + return false; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expires, $jti) + { + foreach ($this->jti as $storedJti) { + if ($storedJti['issuer'] == $client_id && $storedJti['subject'] == $subject && $storedJti['audience'] == $audience && $storedJti['expires'] == $expires && $storedJti['jti'] == $jti) { + return array( + 'issuer' => $storedJti['issuer'], + 'subject' => $storedJti['subject'], + 'audience' => $storedJti['audience'], + 'expires' => $storedJti['expires'], + 'jti' => $storedJti['jti'] + ); + } + } + + return null; + } + + public function setJti($client_id, $subject, $audience, $expires, $jti) + { + $this->jti[] = array('issuer' => $client_id, 'subject' => $subject, 'audience' => $audience, 'expires' => $expires, 'jti' => $jti); + } + + /*PublicKeyInterface */ + public function getPublicKey($client_id = null) + { + if (isset($this->keys[$client_id])) { + return $this->keys[$client_id]['public_key']; + } + + // use a global encryption pair + if (isset($this->keys['public_key'])) { + return $this->keys['public_key']; + } + + return false; + } + + public function getPrivateKey($client_id = null) + { + if (isset($this->keys[$client_id])) { + return $this->keys[$client_id]['private_key']; + } + + // use a global encryption pair + if (isset($this->keys['private_key'])) { + return $this->keys['private_key']; + } + + return false; + } + + public function getEncryptionAlgorithm($client_id = null) + { + if (isset($this->keys[$client_id]['encryption_algorithm'])) { + return $this->keys[$client_id]['encryption_algorithm']; + } + + // use a global encryption algorithm + if (isset($this->keys['encryption_algorithm'])) { + return $this->keys['encryption_algorithm']; + } + + return 'RS256'; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Mongo.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Mongo.php new file mode 100644 index 000000000..eea06e315 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Mongo.php @@ -0,0 +1,392 @@ +<?php + +namespace OAuth2\Storage; + +use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; + +/** + * Simple MongoDB storage for all storage types + * + * NOTE: This class is meant to get users started + * quickly. If your application requires further + * customization, extend this class or create your own. + * + * NOTE: Passwords are stored in plaintext, which is never + * a good idea. Be sure to override this for your application + * + * @author Julien Chaumond <chaumond@gmail.com> + */ +class Mongo implements AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + PublicKeyInterface, + OpenIDAuthorizationCodeInterface +{ + protected $db; + protected $config; + + public function __construct($connection, $config = array()) + { + if ($connection instanceof \MongoDB) { + $this->db = $connection; + } else { + if (!is_array($connection)) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Mongo must be an instance of MongoDB or a configuration array'); + } + $server = sprintf('mongodb://%s:%d', $connection['host'], $connection['port']); + $m = new \MongoClient($server); + $this->db = $m->{$connection['database']}; + } + + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'key_table' => 'oauth_keys', + 'jwt_table' => 'oauth_jwt', + ), $config); + } + + // Helper function to access a MongoDB collection by `type`: + protected function collection($name) + { + return $this->db->{$this->config[$name]}; + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if ($result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { + return $result['client_secret'] == $client_secret; + } + + return false; + } + + public function isPublicClient($client_id) + { + if (!$result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { + return false; + } + + return empty($result['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + $result = $this->collection('client_table')->findOne(array('client_id' => $client_id)); + + return is_null($result) ? false : $result; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + if ($this->getClientDetails($client_id)) { + $this->collection('client_table')->update( + array('client_id' => $client_id), + array('$set' => array( + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + )) + ); + } else { + $client = array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + ); + $this->collection('client_table')->insert($client); + } + + return true; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + $token = $this->collection('access_token_table')->findOne(array('access_token' => $access_token)); + + return is_null($token) ? false : $token; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // if it exists, update it. + if ($this->getAccessToken($access_token)) { + $this->collection('access_token_table')->update( + array('access_token' => $access_token), + array('$set' => array( + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + )) + ); + } else { + $token = array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + ); + $this->collection('access_token_table')->insert($token); + } + + return true; + } + + public function unsetAccessToken($access_token) + { + $result = $this->collection('access_token_table')->remove(array( + 'access_token' => $access_token + ), array('w' => 1)); + + return $result['n'] > 0; + } + + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + $code = $this->collection('code_table')->findOne(array('authorization_code' => $code)); + + return is_null($code) ? false : $code; + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $this->collection('code_table')->update( + array('authorization_code' => $code), + array('$set' => array( + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + )) + ); + } else { + $token = array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + ); + $this->collection('code_table')->insert($token); + } + + return true; + } + + public function expireAuthorizationCode($code) + { + $this->collection('code_table')->remove(array('authorization_code' => $code)); + + return true; + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + + return false; + } + + public function getUserDetails($username) + { + if ($user = $this->getUser($username)) { + $user['user_id'] = $user['username']; + } + + return $user; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + $token = $this->collection('refresh_token_table')->findOne(array('refresh_token' => $refresh_token)); + + return is_null($token) ? false : $token; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + $token = array( + 'refresh_token' => $refresh_token, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope + ); + $this->collection('refresh_token_table')->insert($token); + + return true; + } + + public function unsetRefreshToken($refresh_token) + { + $result = $this->collection('refresh_token_table')->remove(array( + 'refresh_token' => $refresh_token + ), array('w' => 1)); + + return $result['n'] > 0; + } + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == $password; + } + + public function getUser($username) + { + $result = $this->collection('user_table')->findOne(array('username' => $username)); + + return is_null($result) ? false : $result; + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + if ($this->getUser($username)) { + $this->collection('user_table')->update( + array('username' => $username), + array('$set' => array( + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + )) + ); + } else { + $user = array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + ); + $this->collection('user_table')->insert($user); + } + + return true; + } + + public function getClientKey($client_id, $subject) + { + $result = $this->collection('jwt_table')->findOne(array( + 'client_id' => $client_id, + 'subject' => $subject + )); + + return is_null($result) ? false : $result['key']; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs mongodb implementation. + throw new \Exception('getJti() for the MongoDB driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs mongodb implementation. + throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.'); + } + + public function getPublicKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['public_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['public_key']; + } + + public function getPrivateKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['private_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['private_key']; + } + + public function getEncryptionAlgorithm($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['encryption_algorithm']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? 'RS256' : $result['encryption_algorithm']; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/MongoDB.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/MongoDB.php new file mode 100644 index 000000000..64f740fc1 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/MongoDB.php @@ -0,0 +1,380 @@ +<?php + +namespace OAuth2\Storage; + +use MongoDB\Client; +use MongoDB\Database; +use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; + +/** + * Simple MongoDB storage for all storage types + * + * NOTE: This class is meant to get users started + * quickly. If your application requires further + * customization, extend this class or create your own. + * + * NOTE: Passwords are stored in plaintext, which is never + * a good idea. Be sure to override this for your application + * + * @author Julien Chaumond <chaumond@gmail.com> + */ +class MongoDB implements AuthorizationCodeInterface, + UserCredentialsInterface, + AccessTokenInterface, + ClientCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + PublicKeyInterface, + OpenIDAuthorizationCodeInterface +{ + protected $db; + protected $config; + + public function __construct($connection, $config = array()) + { + if ($connection instanceof Database) { + $this->db = $connection; + } else { + if (!is_array($connection)) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Mongo must be an instance of MongoDB\Database or a configuration array'); + } + $server = sprintf('mongodb://%s:%d', $connection['host'], $connection['port']); + $m = new Client($server); + $this->db = $m->selectDatabase($connection['database']); + } + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'jwt_table' => 'oauth_jwt', + 'jti_table' => 'oauth_jti', + 'scope_table' => 'oauth_scopes', + 'key_table' => 'oauth_keys', + ), $config); + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if ($result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { + return $result['client_secret'] == $client_secret; + } + return false; + } + + public function isPublicClient($client_id) + { + if (!$result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { + return false; + } + return empty($result['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + $result = $this->collection('client_table')->findOne(array('client_id' => $client_id)); + return is_null($result) ? false : $result; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + if ($this->getClientDetails($client_id)) { + $result = $this->collection('client_table')->updateOne( + array('client_id' => $client_id), + array('$set' => array( + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + )) + ); + return $result->getMatchedCount() > 0; + } + $client = array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + ); + $result = $this->collection('client_table')->insertOne($client); + return $result->getInsertedCount() > 0; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + return in_array($grant_type, $grant_types); + } + // if grant_types are not defined, then none are restricted + return true; + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + $token = $this->collection('access_token_table')->findOne(array('access_token' => $access_token)); + return is_null($token) ? false : $token; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // if it exists, update it. + if ($this->getAccessToken($access_token)) { + $result = $this->collection('access_token_table')->updateOne( + array('access_token' => $access_token), + array('$set' => array( + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + )) + ); + return $result->getMatchedCount() > 0; + } + $token = array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + ); + $result = $this->collection('access_token_table')->insertOne($token); + return $result->getInsertedCount() > 0; + } + + public function unsetAccessToken($access_token) + { + $result = $this->collection('access_token_table')->deleteOne(array( + 'access_token' => $access_token + )); + return $result->getDeletedCount() > 0; + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + $code = $this->collection('code_table')->findOne(array( + 'authorization_code' => $code + )); + return is_null($code) ? false : $code; + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $result = $this->collection('code_table')->updateOne( + array('authorization_code' => $code), + array('$set' => array( + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + )) + ); + return $result->getMatchedCount() > 0; + } + $token = array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + ); + $result = $this->collection('code_table')->insertOne($token); + return $result->getInsertedCount() > 0; + } + + public function expireAuthorizationCode($code) + { + $result = $this->collection('code_table')->deleteOne(array( + 'authorization_code' => $code + )); + return $result->getDeletedCount() > 0; + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + return false; + } + + public function getUserDetails($username) + { + if ($user = $this->getUser($username)) { + $user['user_id'] = $user['username']; + } + return $user; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + $token = $this->collection('refresh_token_table')->findOne(array( + 'refresh_token' => $refresh_token + )); + return is_null($token) ? false : $token; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + $token = array( + 'refresh_token' => $refresh_token, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope + ); + $result = $this->collection('refresh_token_table')->insertOne($token); + return $result->getInsertedCount() > 0; + } + + public function unsetRefreshToken($refresh_token) + { + $result = $this->collection('refresh_token_table')->deleteOne(array( + 'refresh_token' => $refresh_token + )); + return $result->getDeletedCount() > 0; + } + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == $password; + } + + public function getUser($username) + { + $result = $this->collection('user_table')->findOne(array('username' => $username)); + return is_null($result) ? false : $result; + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + if ($this->getUser($username)) { + $result = $this->collection('user_table')->updateOne( + array('username' => $username), + array('$set' => array( + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + )) + ); + + return $result->getMatchedCount() > 0; + } + + $user = array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + ); + $result = $this->collection('user_table')->insertOne($user); + return $result->getInsertedCount() > 0; + } + + public function getClientKey($client_id, $subject) + { + $result = $this->collection('jwt_table')->findOne(array( + 'client_id' => $client_id, + 'subject' => $subject + )); + return is_null($result) ? false : $result['key']; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + return null; + } + + public function getJti($client_id, $subject, $audience, $expires, $jti) + { + //TODO: Needs mongodb implementation. + throw new \Exception('getJti() for the MongoDB driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expires, $jti) + { + //TODO: Needs mongodb implementation. + throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.'); + } + + public function getPublicKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['public_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['public_key']; + } + + public function getPrivateKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['private_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['private_key']; + } + + public function getEncryptionAlgorithm($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['encryption_algorithm']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? 'RS256' : $result['encryption_algorithm']; + } + + // Helper function to access a MongoDB collection by `type`: + protected function collection($name) + { + return $this->db->{$this->config[$name]}; + } +}
\ No newline at end of file diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Pdo.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Pdo.php new file mode 100644 index 000000000..ae5107e29 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Pdo.php @@ -0,0 +1,553 @@ +<?php + +namespace OAuth2\Storage; + +use OAuth2\OpenID\Storage\UserClaimsInterface; +use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; + +/** + * Simple PDO storage for all storage types + * + * NOTE: This class is meant to get users started + * quickly. If your application requires further + * customization, extend this class or create your own. + * + * NOTE: Passwords are stored in plaintext, which is never + * a good idea. Be sure to override this for your application + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +class Pdo implements + AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + ScopeInterface, + PublicKeyInterface, + UserClaimsInterface, + OpenIDAuthorizationCodeInterface +{ + protected $db; + protected $config; + + public function __construct($connection, $config = array()) + { + if (!$connection instanceof \PDO) { + if (is_string($connection)) { + $connection = array('dsn' => $connection); + } + if (!is_array($connection)) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Pdo must be an instance of PDO, a DSN string, or a configuration array'); + } + if (!isset($connection['dsn'])) { + throw new \InvalidArgumentException('configuration array must contain "dsn"'); + } + // merge optional parameters + $connection = array_merge(array( + 'username' => null, + 'password' => null, + 'options' => array(), + ), $connection); + $connection = new \PDO($connection['dsn'], $connection['username'], $connection['password'], $connection['options']); + } + $this->db = $connection; + + // debugging + $connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'jwt_table' => 'oauth_jwt', + 'jti_table' => 'oauth_jti', + 'scope_table' => 'oauth_scopes', + 'public_key_table' => 'oauth_public_keys', + ), $config); + } + + /* OAuth2\Storage\ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); + $stmt->execute(compact('client_id')); + $result = $stmt->fetch(\PDO::FETCH_ASSOC); + + // make this extensible + return $result && $result['client_secret'] == $client_secret; + } + + public function isPublicClient($client_id) + { + $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); + $stmt->execute(compact('client_id')); + + if (!$result = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return false; + } + + return empty($result['client_secret']); + } + + /* OAuth2\Storage\ClientInterface */ + public function getClientDetails($client_id) + { + $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); + $stmt->execute(compact('client_id')); + + return $stmt->fetch(\PDO::FETCH_ASSOC); + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + // if it exists, update it. + if ($this->getClientDetails($client_id)) { + $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_secret=:client_secret, redirect_uri=:redirect_uri, grant_types=:grant_types, scope=:scope, user_id=:user_id where client_id=:client_id', $this->config['client_table'])); + } else { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (client_id, client_secret, redirect_uri, grant_types, scope, user_id) VALUES (:client_id, :client_secret, :redirect_uri, :grant_types, :scope, :user_id)', $this->config['client_table'])); + } + + return $stmt->execute(compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id')); + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, (array) $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* OAuth2\Storage\AccessTokenInterface */ + public function getAccessToken($access_token) + { + $stmt = $this->db->prepare(sprintf('SELECT * from %s where access_token = :access_token', $this->config['access_token_table'])); + + $token = $stmt->execute(compact('access_token')); + if ($token = $stmt->fetch(\PDO::FETCH_ASSOC)) { + // convert date string back to timestamp + $token['expires'] = strtotime($token['expires']); + } + + return $token; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + // if it exists, update it. + if ($this->getAccessToken($access_token)) { + $stmt = $this->db->prepare(sprintf('UPDATE %s SET client_id=:client_id, expires=:expires, user_id=:user_id, scope=:scope where access_token=:access_token', $this->config['access_token_table'])); + } else { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (access_token, client_id, expires, user_id, scope) VALUES (:access_token, :client_id, :expires, :user_id, :scope)', $this->config['access_token_table'])); + } + + return $stmt->execute(compact('access_token', 'client_id', 'user_id', 'expires', 'scope')); + } + + public function unsetAccessToken($access_token) + { + $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE access_token = :access_token', $this->config['access_token_table'])); + + $stmt->execute(compact('access_token')); + + return $stmt->rowCount() > 0; + } + + /* OAuth2\Storage\AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + $stmt = $this->db->prepare(sprintf('SELECT * from %s where authorization_code = :code', $this->config['code_table'])); + $stmt->execute(compact('code')); + + if ($code = $stmt->fetch(\PDO::FETCH_ASSOC)) { + // convert date string back to timestamp + $code['expires'] = strtotime($code['expires']); + } + + return $code; + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + if (func_num_args() > 6) { + // we are calling with an id token + return call_user_func_array(array($this, 'setAuthorizationCodeWithIdToken'), func_get_args()); + } + + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope where authorization_code=:code', $this->config['code_table'])); + } else { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope)', $this->config['code_table'])); + } + + return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope')); + } + + private function setAuthorizationCodeWithIdToken($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope, id_token =:id_token where authorization_code=:code', $this->config['code_table'])); + } else { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope, id_token) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope, :id_token)', $this->config['code_table'])); + } + + return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token')); + } + + public function expireAuthorizationCode($code) + { + $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE authorization_code = :code', $this->config['code_table'])); + + return $stmt->execute(compact('code')); + } + + /* OAuth2\Storage\UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + + return false; + } + + public function getUserDetails($username) + { + return $this->getUser($username); + } + + /* UserClaimsInterface */ + public function getUserClaims($user_id, $claims) + { + if (!$userDetails = $this->getUserDetails($user_id)) { + return false; + } + + $claims = explode(' ', trim($claims)); + $userClaims = array(); + + // for each requested claim, if the user has the claim, set it in the response + $validClaims = explode(' ', self::VALID_CLAIMS); + foreach ($validClaims as $validClaim) { + if (in_array($validClaim, $claims)) { + if ($validClaim == 'address') { + // address is an object with subfields + $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); + } else { + $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails)); + } + } + } + + return $userClaims; + } + + protected function getUserClaim($claim, $userDetails) + { + $userClaims = array(); + $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim))); + $claimValues = explode(' ', $claimValuesString); + + foreach ($claimValues as $value) { + $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null; + } + + return $userClaims; + } + + /* OAuth2\Storage\RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + $stmt = $this->db->prepare(sprintf('SELECT * FROM %s WHERE refresh_token = :refresh_token', $this->config['refresh_token_table'])); + + $token = $stmt->execute(compact('refresh_token')); + if ($token = $stmt->fetch(\PDO::FETCH_ASSOC)) { + // convert expires to epoch time + $token['expires'] = strtotime($token['expires']); + } + + return $token; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + // convert expires to datestring + $expires = date('Y-m-d H:i:s', $expires); + + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (refresh_token, client_id, user_id, expires, scope) VALUES (:refresh_token, :client_id, :user_id, :expires, :scope)', $this->config['refresh_token_table'])); + + return $stmt->execute(compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope')); + } + + public function unsetRefreshToken($refresh_token) + { + $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE refresh_token = :refresh_token', $this->config['refresh_token_table'])); + + $stmt->execute(compact('refresh_token')); + + return $stmt->rowCount() > 0; + } + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == $this->hashPassword($password); + } + + // use a secure hashing algorithm when storing passwords. Override this for your application + protected function hashPassword($password) + { + return sha1($password); + } + + public function getUser($username) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT * from %s where username=:username', $this->config['user_table'])); + $stmt->execute(array('username' => $username)); + + if (!$userInfo = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return false; + } + + // the default behavior is to use "username" as the user_id + return array_merge(array( + 'user_id' => $username + ), $userInfo); + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + // do not store in plaintext + $password = $this->hashPassword($password); + + // if it exists, update it. + if ($this->getUser($username)) { + $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET password=:password, first_name=:firstName, last_name=:lastName where username=:username', $this->config['user_table'])); + } else { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (username, password, first_name, last_name) VALUES (:username, :password, :firstName, :lastName)', $this->config['user_table'])); + } + + return $stmt->execute(compact('username', 'password', 'firstName', 'lastName')); + } + + /* ScopeInterface */ + public function scopeExists($scope) + { + $scope = explode(' ', $scope); + $whereIn = implode(',', array_fill(0, count($scope), '?')); + $stmt = $this->db->prepare(sprintf('SELECT count(scope) as count FROM %s WHERE scope IN (%s)', $this->config['scope_table'], $whereIn)); + $stmt->execute($scope); + + if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return $result['count'] == count($scope); + } + + return false; + } + + public function getDefaultScope($client_id = null) + { + $stmt = $this->db->prepare(sprintf('SELECT scope FROM %s WHERE is_default=:is_default', $this->config['scope_table'])); + $stmt->execute(array('is_default' => true)); + + if ($result = $stmt->fetchAll(\PDO::FETCH_ASSOC)) { + $defaultScope = array_map(function ($row) { + return $row['scope']; + }, $result); + + return implode(' ', $defaultScope); + } + + return null; + } + + /* JWTBearerInterface */ + public function getClientKey($client_id, $subject) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT public_key from %s where client_id=:client_id AND subject=:subject', $this->config['jwt_table'])); + + $stmt->execute(array('client_id' => $client_id, 'subject' => $subject)); + + return $stmt->fetchColumn(); + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expires, $jti) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT * FROM %s WHERE issuer=:client_id AND subject=:subject AND audience=:audience AND expires=:expires AND jti=:jti', $this->config['jti_table'])); + + $stmt->execute(compact('client_id', 'subject', 'audience', 'expires', 'jti')); + + if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return array( + 'issuer' => $result['issuer'], + 'subject' => $result['subject'], + 'audience' => $result['audience'], + 'expires' => $result['expires'], + 'jti' => $result['jti'], + ); + } + + return null; + } + + public function setJti($client_id, $subject, $audience, $expires, $jti) + { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (issuer, subject, audience, expires, jti) VALUES (:client_id, :subject, :audience, :expires, :jti)', $this->config['jti_table'])); + + return $stmt->execute(compact('client_id', 'subject', 'audience', 'expires', 'jti')); + } + + /* PublicKeyInterface */ + public function getPublicKey($client_id = null) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT public_key FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table'])); + + $stmt->execute(compact('client_id')); + if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return $result['public_key']; + } + } + + public function getPrivateKey($client_id = null) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT private_key FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table'])); + + $stmt->execute(compact('client_id')); + if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return $result['private_key']; + } + } + + public function getEncryptionAlgorithm($client_id = null) + { + $stmt = $this->db->prepare($sql = sprintf('SELECT encryption_algorithm FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table'])); + + $stmt->execute(compact('client_id')); + if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return $result['encryption_algorithm']; + } + + return 'RS256'; + } + + /** + * DDL to create OAuth2 database and tables for PDO storage + * + * @see https://github.com/dsquier/oauth2-server-php-mysql + */ + public function getBuildSql($dbName = 'oauth2_server_php') + { + $sql = " + CREATE TABLE {$this->config['client_table']} ( + client_id VARCHAR(80) NOT NULL, + client_secret VARCHAR(80), + redirect_uri VARCHAR(2000), + grant_types VARCHAR(80), + scope VARCHAR(4000), + user_id VARCHAR(80), + PRIMARY KEY (client_id) + ); + + CREATE TABLE {$this->config['access_token_table']} ( + access_token VARCHAR(40) NOT NULL, + client_id VARCHAR(80) NOT NULL, + user_id VARCHAR(80), + expires TIMESTAMP NOT NULL, + scope VARCHAR(4000), + PRIMARY KEY (access_token) + ); + + CREATE TABLE {$this->config['code_table']} ( + authorization_code VARCHAR(40) NOT NULL, + client_id VARCHAR(80) NOT NULL, + user_id VARCHAR(80), + redirect_uri VARCHAR(2000), + expires TIMESTAMP NOT NULL, + scope VARCHAR(4000), + id_token VARCHAR(1000), + PRIMARY KEY (authorization_code) + ); + + CREATE TABLE {$this->config['refresh_token_table']} ( + refresh_token VARCHAR(40) NOT NULL, + client_id VARCHAR(80) NOT NULL, + user_id VARCHAR(80), + expires TIMESTAMP NOT NULL, + scope VARCHAR(4000), + PRIMARY KEY (refresh_token) + ); + + CREATE TABLE {$this->config['user_table']} ( + username VARCHAR(80), + password VARCHAR(80), + first_name VARCHAR(80), + last_name VARCHAR(80), + email VARCHAR(80), + email_verified BOOLEAN, + scope VARCHAR(4000) + ); + + CREATE TABLE {$this->config['scope_table']} ( + scope VARCHAR(80) NOT NULL, + is_default BOOLEAN, + PRIMARY KEY (scope) + ); + + CREATE TABLE {$this->config['jwt_table']} ( + client_id VARCHAR(80) NOT NULL, + subject VARCHAR(80), + public_key VARCHAR(2000) NOT NULL + ); + + CREATE TABLE {$this->config['jti_table']} ( + issuer VARCHAR(80) NOT NULL, + subject VARCHAR(80), + audience VARCHAR(80), + expires TIMESTAMP NOT NULL, + jti VARCHAR(2000) NOT NULL + ); + + CREATE TABLE {$this->config['public_key_table']} ( + client_id VARCHAR(80), + public_key VARCHAR(2000), + private_key VARCHAR(2000), + encryption_algorithm VARCHAR(100) DEFAULT 'RS256' + ) +"; + + return $sql; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/PublicKeyInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/PublicKeyInterface.php new file mode 100644 index 000000000..108418d3a --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/PublicKeyInterface.php @@ -0,0 +1,16 @@ +<?php + +namespace OAuth2\Storage; + +/** + * Implement this interface to specify where the OAuth2 Server + * should get public/private key information + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +interface PublicKeyInterface +{ + public function getPublicKey($client_id = null); + public function getPrivateKey($client_id = null); + public function getEncryptionAlgorithm($client_id = null); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Redis.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Redis.php new file mode 100644 index 000000000..e6294e22d --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Redis.php @@ -0,0 +1,321 @@ +<?php + +namespace OAuth2\Storage; + +use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; + +/** + * redis storage for all storage types + * + * To use, install "predis/predis" via composer + * + * Register client: + * <code> + * $storage = new OAuth2\Storage\Redis($redis); + * $storage->setClientDetails($client_id, $client_secret, $redirect_uri); + * </code> + */ +class Redis implements AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + ScopeInterface, + OpenIDAuthorizationCodeInterface +{ + + private $cache; + + /* The redis client */ + protected $redis; + + /* Configuration array */ + protected $config; + + /** + * Redis Storage! + * + * @param \Predis\Client $redis + * @param array $config + */ + public function __construct($redis, $config=array()) + { + $this->redis = $redis; + $this->config = array_merge(array( + 'client_key' => 'oauth_clients:', + 'access_token_key' => 'oauth_access_tokens:', + 'refresh_token_key' => 'oauth_refresh_tokens:', + 'code_key' => 'oauth_authorization_codes:', + 'user_key' => 'oauth_users:', + 'jwt_key' => 'oauth_jwt:', + 'scope_key' => 'oauth_scopes:', + ), $config); + } + + protected function getValue($key) + { + if ( isset($this->cache[$key]) ) { + return $this->cache[$key]; + } + $value = $this->redis->get($key); + if ( isset($value) ) { + return json_decode($value, true); + } else { + return false; + } + } + + protected function setValue($key, $value, $expire=0) + { + $this->cache[$key] = $value; + $str = json_encode($value); + if ($expire > 0) { + $seconds = $expire - time(); + $ret = $this->redis->setex($key, $seconds, $str); + } else { + $ret = $this->redis->set($key, $str); + } + + // check that the key was set properly + // if this fails, an exception will usually thrown, so this step isn't strictly necessary + return is_bool($ret) ? $ret : $ret->getPayload() == 'OK'; + } + + protected function expireValue($key) + { + unset($this->cache[$key]); + + return $this->redis->del($key); + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + return $this->getValue($this->config['code_key'] . $code); + } + + public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + return $this->setValue( + $this->config['code_key'] . $authorization_code, + compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'), + $expires + ); + } + + public function expireAuthorizationCode($code) + { + $key = $this->config['code_key'] . $code; + unset($this->cache[$key]); + + return $this->expireValue($key); + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + $user = $this->getUserDetails($username); + + return $user && $user['password'] === $password; + } + + public function getUserDetails($username) + { + return $this->getUser($username); + } + + public function getUser($username) + { + if (!$userInfo = $this->getValue($this->config['user_key'] . $username)) { + return false; + } + + // the default behavior is to use "username" as the user_id + return array_merge(array( + 'user_id' => $username, + ), $userInfo); + } + + public function setUser($username, $password, $first_name = null, $last_name = null) + { + return $this->setValue( + $this->config['user_key'] . $username, + compact('username', 'password', 'first_name', 'last_name') + ); + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if (!$client = $this->getClientDetails($client_id)) { + return false; + } + + return isset($client['client_secret']) + && $client['client_secret'] == $client_secret; + } + + public function isPublicClient($client_id) + { + if (!$client = $this->getClientDetails($client_id)) { + return false; + } + + return empty($client['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + return $this->getValue($this->config['client_key'] . $client_id); + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + return $this->setValue( + $this->config['client_key'] . $client_id, + compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id') + ); + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + + return in_array($grant_type, (array) $grant_types); + } + + // if grant_types are not defined, then none are restricted + return true; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + return $this->getValue($this->config['refresh_token_key'] . $refresh_token); + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + return $this->setValue( + $this->config['refresh_token_key'] . $refresh_token, + compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'), + $expires + ); + } + + public function unsetRefreshToken($refresh_token) + { + $result = $this->expireValue($this->config['refresh_token_key'] . $refresh_token); + + return $result > 0; + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + return $this->getValue($this->config['access_token_key'].$access_token); + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + return $this->setValue( + $this->config['access_token_key'].$access_token, + compact('access_token', 'client_id', 'user_id', 'expires', 'scope'), + $expires + ); + } + + public function unsetAccessToken($access_token) + { + $result = $this->expireValue($this->config['access_token_key'] . $access_token); + + return $result > 0; + } + + /* ScopeInterface */ + public function scopeExists($scope) + { + $scope = explode(' ', $scope); + + $result = $this->getValue($this->config['scope_key'].'supported:global'); + + $supportedScope = explode(' ', (string) $result); + + return (count(array_diff($scope, $supportedScope)) == 0); + } + + public function getDefaultScope($client_id = null) + { + if (is_null($client_id) || !$result = $this->getValue($this->config['scope_key'].'default:'.$client_id)) { + $result = $this->getValue($this->config['scope_key'].'default:global'); + } + + return $result; + } + + public function setScope($scope, $client_id = null, $type = 'supported') + { + if (!in_array($type, array('default', 'supported'))) { + throw new \InvalidArgumentException('"$type" must be one of "default", "supported"'); + } + + if (is_null($client_id)) { + $key = $this->config['scope_key'].$type.':global'; + } else { + $key = $this->config['scope_key'].$type.':'.$client_id; + } + + return $this->setValue($key, $scope); + } + + /*JWTBearerInterface */ + public function getClientKey($client_id, $subject) + { + if (!$jwt = $this->getValue($this->config['jwt_key'] . $client_id)) { + return false; + } + + if (isset($jwt['subject']) && $jwt['subject'] == $subject) { + return $jwt['key']; + } + + return null; + } + + public function setClientKey($client_id, $key, $subject = null) + { + return $this->setValue($this->config['jwt_key'] . $client_id, array( + 'key' => $key, + 'subject' => $subject + )); + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + + return null; + } + + public function getJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs redis implementation. + throw new \Exception('getJti() for the Redis driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expiration, $jti) + { + //TODO: Needs redis implementation. + throw new \Exception('setJti() for the Redis driver is currently unimplemented.'); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/RefreshTokenInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/RefreshTokenInterface.php new file mode 100644 index 000000000..e6407e440 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/RefreshTokenInterface.php @@ -0,0 +1,82 @@ +<?php + +namespace OAuth2\Storage; + +/** + * Implement this interface to specify where the OAuth2 Server + * should get/save refresh tokens for the "Refresh Token" + * grant type + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +interface RefreshTokenInterface +{ + /** + * Grant refresh access tokens. + * + * Retrieve the stored data for the given refresh token. + * + * Required for OAuth2::GRANT_TYPE_REFRESH_TOKEN. + * + * @param $refresh_token + * Refresh token to be check with. + * + * @return + * An associative array as below, and NULL if the refresh_token is + * invalid: + * - refresh_token: Refresh token identifier. + * - client_id: Client identifier. + * - user_id: User identifier. + * - expires: Expiration unix timestamp, or 0 if the token doesn't expire. + * - scope: (optional) Scope values in space-separated string. + * + * @see http://tools.ietf.org/html/rfc6749#section-6 + * + * @ingroup oauth2_section_6 + */ + public function getRefreshToken($refresh_token); + + /** + * Take the provided refresh token values and store them somewhere. + * + * This function should be the storage counterpart to getRefreshToken(). + * + * If storage fails for some reason, we're not currently checking for + * any sort of success/failure, so you should bail out of the script + * and provide a descriptive fail message. + * + * Required for OAuth2::GRANT_TYPE_REFRESH_TOKEN. + * + * @param $refresh_token + * Refresh token to be stored. + * @param $client_id + * Client identifier to be stored. + * @param $user_id + * User identifier to be stored. + * @param $expires + * Expiration timestamp to be stored. 0 if the token doesn't expire. + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * + * @ingroup oauth2_section_6 + */ + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null); + + /** + * Expire a used refresh token. + * + * This is not explicitly required in the spec, but is almost implied. + * After granting a new refresh token, the old one is no longer useful and + * so should be forcibly expired in the data store so it can't be used again. + * + * If storage fails for some reason, we're not currently checking for + * any sort of success/failure, so you should bail out of the script + * and provide a descriptive fail message. + * + * @param $refresh_token + * Refresh token to be expired. + * + * @ingroup oauth2_section_6 + */ + public function unsetRefreshToken($refresh_token); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ScopeInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ScopeInterface.php new file mode 100644 index 000000000..a8292269b --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ScopeInterface.php @@ -0,0 +1,46 @@ +<?php + +namespace OAuth2\Storage; + +/** + * Implement this interface to specify where the OAuth2 Server + * should retrieve data involving the relevent scopes associated + * with this implementation. + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +interface ScopeInterface +{ + /** + * Check if the provided scope exists. + * + * @param $scope + * A space-separated string of scopes. + * + * @return + * TRUE if it exists, FALSE otherwise. + */ + public function scopeExists($scope); + + /** + * The default scope to use in the event the client + * does not request one. By returning "false", a + * request_error is returned by the server to force a + * scope request by the client. By returning "null", + * opt out of requiring scopes + * + * @param $client_id + * An optional client id that can be used to return customized default scopes. + * + * @return + * string representation of default scope, null if + * scopes are not defined, or false to force scope + * request by the client + * + * ex: + * 'default' + * ex: + * null + */ + public function getDefaultScope($client_id = null); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/UserCredentialsInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/UserCredentialsInterface.php new file mode 100644 index 000000000..6e0fd7bad --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/UserCredentialsInterface.php @@ -0,0 +1,52 @@ +<?php + +namespace OAuth2\Storage; + +/** + * Implement this interface to specify where the OAuth2 Server + * should retrieve user credentials for the + * "Resource Owner Password Credentials" grant type + * + * @author Brent Shaffer <bshafs at gmail dot com> + */ +interface UserCredentialsInterface +{ + /** + * Grant access tokens for basic user credentials. + * + * Check the supplied username and password for validity. + * + * You can also use the $client_id param to do any checks required based + * on a client, if you need that. + * + * Required for OAuth2::GRANT_TYPE_USER_CREDENTIALS. + * + * @param $username + * Username to be check with. + * @param $password + * Password to be check with. + * + * @return + * TRUE if the username and password are valid, and FALSE if it isn't. + * Moreover, if the username and password are valid, and you want to + * + * @see http://tools.ietf.org/html/rfc6749#section-4.3 + * + * @ingroup oauth2_section_4 + */ + public function checkUserCredentials($username, $password); + + /** + * @return + * ARRAY the associated "user_id" and optional "scope" values + * This function MUST return FALSE if the requested user does not exist or is + * invalid. "scope" is a space-separated list of restricted scopes. + * @code + * return array( + * "user_id" => USER_ID, // REQUIRED user_id to be stored with the authorization code or access token + * "scope" => SCOPE // OPTIONAL space-separated list of restricted scopes + * ); + * @endcode + */ + public function getUserDetails($username); +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Bearer.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Bearer.php new file mode 100644 index 000000000..8ac8596ac --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Bearer.php @@ -0,0 +1,130 @@ +<?php + +namespace OAuth2\TokenType; + +use OAuth2\RequestInterface; +use OAuth2\ResponseInterface; + +/** +* +*/ +class Bearer implements TokenTypeInterface +{ + private $config; + + public function __construct(array $config = array()) + { + $this->config = array_merge(array( + 'token_param_name' => 'access_token', + 'token_bearer_header_name' => 'Bearer', + ), $config); + } + + public function getTokenType() + { + return 'Bearer'; + } + + /** + * Check if the request has supplied token + * + * @see https://github.com/bshaffer/oauth2-server-php/issues/349#issuecomment-37993588 + */ + public function requestHasToken(RequestInterface $request) + { + $headers = $request->headers('AUTHORIZATION'); + + // check the header, then the querystring, then the request body + return !empty($headers) || (bool) ($request->request($this->config['token_param_name'])) || (bool) ($request->query($this->config['token_param_name'])); + } + + /** + * This is a convenience function that can be used to get the token, which can then + * be passed to getAccessTokenData(). The constraints specified by the draft are + * attempted to be adheared to in this method. + * + * As per the Bearer spec (draft 8, section 2) - there are three ways for a client + * to specify the bearer token, in order of preference: Authorization Header, + * POST and GET. + * + * NB: Resource servers MUST accept tokens via the Authorization scheme + * (http://tools.ietf.org/html/rfc6750#section-2). + * + * @todo Should we enforce TLS/SSL in this function? + * + * @see http://tools.ietf.org/html/rfc6750#section-2.1 + * @see http://tools.ietf.org/html/rfc6750#section-2.2 + * @see http://tools.ietf.org/html/rfc6750#section-2.3 + * + * Old Android version bug (at least with version 2.2) + * @see http://code.google.com/p/android/issues/detail?id=6684 + * + */ + public function getAccessTokenParameter(RequestInterface $request, ResponseInterface $response) + { + $headers = $request->headers('AUTHORIZATION'); + + /** + * Ensure more than one method is not used for including an + * access token + * + * @see http://tools.ietf.org/html/rfc6750#section-3.1 + */ + $methodsUsed = !empty($headers) + (bool) ($request->query($this->config['token_param_name'])) + (bool) ($request->request($this->config['token_param_name'])); + if ($methodsUsed > 1) { + $response->setError(400, 'invalid_request', 'Only one method may be used to authenticate at a time (Auth header, GET or POST)'); + + return null; + } + + /** + * If no authentication is provided, set the status code + * to 401 and return no other error information + * + * @see http://tools.ietf.org/html/rfc6750#section-3.1 + */ + if ($methodsUsed == 0) { + $response->setStatusCode(401); + + return null; + } + + // HEADER: Get the access token from the header + if (!empty($headers)) { + if (!preg_match('/' . $this->config['token_bearer_header_name'] . '\s(\S+)/i', $headers, $matches)) { + $response->setError(400, 'invalid_request', 'Malformed auth header'); + + return null; + } + + return $matches[1]; + } + + if ($request->request($this->config['token_param_name'])) { + // // POST: Get the token from POST data + if (!in_array(strtolower($request->server('REQUEST_METHOD')), array('post', 'put'))) { + $response->setError(400, 'invalid_request', 'When putting the token in the body, the method must be POST or PUT', '#section-2.2'); + + return null; + } + + $contentType = $request->server('CONTENT_TYPE'); + if (false !== $pos = strpos($contentType, ';')) { + $contentType = substr($contentType, 0, $pos); + } + + if ($contentType !== null && $contentType != 'application/x-www-form-urlencoded') { + // IETF specifies content-type. NB: Not all webservers populate this _SERVER variable + // @see http://tools.ietf.org/html/rfc6750#section-2.2 + $response->setError(400, 'invalid_request', 'The content type for POST requests must be "application/x-www-form-urlencoded"'); + + return null; + } + + return $request->request($this->config['token_param_name']); + } + + // GET method + return $request->query($this->config['token_param_name']); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Mac.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Mac.php new file mode 100644 index 000000000..fe6a86aa6 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Mac.php @@ -0,0 +1,22 @@ +<?php + +namespace OAuth2\TokenType; + +use OAuth2\RequestInterface; +use OAuth2\ResponseInterface; + +/** +* This is not yet supported! +*/ +class Mac implements TokenTypeInterface +{ + public function getTokenType() + { + return 'mac'; + } + + public function getAccessTokenParameter(RequestInterface $request, ResponseInterface $response) + { + throw new \LogicException("Not supported"); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/TokenTypeInterface.php b/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/TokenTypeInterface.php new file mode 100644 index 000000000..ad77d4a25 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/TokenTypeInterface.php @@ -0,0 +1,21 @@ +<?php + +namespace OAuth2\TokenType; + +use OAuth2\RequestInterface; +use OAuth2\ResponseInterface; + +interface TokenTypeInterface +{ + /** + * Token type identification string + * + * ex: "bearer" or "mac" + */ + public function getTokenType(); + + /** + * Retrieves the token string from the request object + */ + public function getAccessTokenParameter(RequestInterface $request, ResponseInterface $response); +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/AutoloadTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/AutoloadTest.php new file mode 100644 index 000000000..5901bdc42 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/AutoloadTest.php @@ -0,0 +1,16 @@ +<?php + +namespace OAuth2; + +class AutoloadTest extends \PHPUnit_Framework_TestCase +{ + public function testClassesExist() + { + // autoloader is called in test/bootstrap.php + $this->assertTrue(class_exists('OAuth2\Server')); + $this->assertTrue(class_exists('OAuth2\Request')); + $this->assertTrue(class_exists('OAuth2\Response')); + $this->assertTrue(class_exists('OAuth2\GrantType\UserCredentials')); + $this->assertTrue(interface_exists('OAuth2\Storage\AccessTokenInterface')); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/AuthorizeControllerTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/AuthorizeControllerTest.php new file mode 100644 index 000000000..3bfc760e4 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/AuthorizeControllerTest.php @@ -0,0 +1,492 @@ +<?php + +namespace OAuth2\Controller; + +use OAuth2\Storage\Memory; +use OAuth2\Scope; +use OAuth2\Storage\Bootstrap; +use OAuth2\Server; +use OAuth2\GrantType\AuthorizationCode; +use OAuth2\Request; +use OAuth2\Response; +use OAuth2\Request\TestRequest; + +class AuthorizeControllerTest extends \PHPUnit_Framework_TestCase +{ + public function testNoClientIdResponse() + { + $server = $this->getTestServer(); + $request = new Request(); + $server->handleAuthorizeRequest($request, $response = new Response(), false); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_client'); + $this->assertEquals($response->getParameter('error_description'), 'No client id supplied'); + } + + public function testInvalidClientIdResponse() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Fake Client ID', // invalid client id + )); + $server->handleAuthorizeRequest($request, $response = new Response(), false); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_client'); + $this->assertEquals($response->getParameter('error_description'), 'The client id supplied is invalid'); + } + + public function testNoRedirectUriSuppliedOrStoredResponse() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + )); + $server->handleAuthorizeRequest($request, $response = new Response(), false); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_uri'); + $this->assertEquals($response->getParameter('error_description'), 'No redirect URI was supplied or stored'); + } + + public function testNoResponseTypeResponse() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + )); + $server->handleAuthorizeRequest($request, $response = new Response(), false); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + parse_str($parts['query'], $query); + + $this->assertEquals($query['error'], 'invalid_request'); + $this->assertEquals($query['error_description'], 'Invalid or missing response type'); + } + + public function testInvalidResponseTypeResponse() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'invalid', // invalid response type + )); + $server->handleAuthorizeRequest($request, $response = new Response(), false); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + parse_str($parts['query'], $query); + + $this->assertEquals($query['error'], 'invalid_request'); + $this->assertEquals($query['error_description'], 'Invalid or missing response type'); + } + + public function testRedirectUriFragmentResponse() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com#fragment', // valid redirect URI + 'response_type' => 'code', // invalid response type + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_uri'); + $this->assertEquals($response->getParameter('error_description'), 'The redirect URI must not contain a fragment'); + } + + public function testEnforceState() + { + $server = $this->getTestServer(array('enforce_state' => true)); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'code', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + parse_str($parts['query'], $query); + + $this->assertEquals($query['error'], 'invalid_request'); + $this->assertEquals($query['error_description'], 'The state parameter is required'); + } + + public function testDoNotEnforceState() + { + $server = $this->getTestServer(array('enforce_state' => false)); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'code', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $this->assertNotContains('error', $response->getHttpHeader('Location')); + } + + public function testEnforceScope() + { + $server = $this->getTestServer(); + $scopeStorage = new Memory(array('default_scope' => false, 'supported_scopes' => array('testscope'))); + $server->setScopeUtil(new Scope($scopeStorage)); + + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'code', + 'state' => 'xyz', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $parts = parse_url($response->getHttpHeader('Location')); + parse_str($parts['query'], $query); + + $this->assertEquals($query['error'], 'invalid_client'); + $this->assertEquals($query['error_description'], 'This application requires you specify a scope parameter'); + + $request->query['scope'] = 'testscope'; + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $this->assertNotContains('error', $response->getHttpHeader('Location')); + } + + public function testInvalidRedirectUri() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID with Redirect Uri', // valid client id + 'redirect_uri' => 'http://adobe.com', // invalid redirect URI + 'response_type' => 'code', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'redirect_uri_mismatch'); + $this->assertEquals($response->getParameter('error_description'), 'The redirect URI provided is missing or does not match'); + } + + public function testInvalidRedirectUriApprovedByBuggyRegisteredUri() + { + $server = $this->getTestServer(); + $server->setConfig('require_exact_redirect_uri', false); + $request = new Request(array( + 'client_id' => 'Test Client ID with Buggy Redirect Uri', // valid client id + 'redirect_uri' => 'http://adobe.com', // invalid redirect URI + 'response_type' => 'code', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'redirect_uri_mismatch'); + $this->assertEquals($response->getParameter('error_description'), 'The redirect URI provided is missing or does not match'); + } + + public function testNoRedirectUriWithMultipleRedirectUris() + { + $server = $this->getTestServer(); + + // create a request with no "redirect_uri" in querystring + $request = new Request(array( + 'client_id' => 'Test Client ID with Multiple Redirect Uris', // valid client id + 'response_type' => 'code', + )); + + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_uri'); + $this->assertEquals($response->getParameter('error_description'), 'A redirect URI must be supplied when multiple redirect URIs are registered'); + } + + public function testRedirectUriWithValidRedirectUri() + { + $server = $this->getTestServer(); + + // create a request with no "redirect_uri" in querystring + $request = new Request(array( + 'client_id' => 'Test Client ID with Redirect Uri Parts', // valid client id + 'response_type' => 'code', + 'redirect_uri' => 'http://user:pass@brentertainment.com:2222/authorize/cb?auth_type=oauth&test=true', + 'state' => 'xyz', + )); + + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $this->assertContains('code', $response->getHttpHeader('Location')); + } + + public function testRedirectUriWithDifferentQueryAndExactMatchRequired() + { + $server = $this->getTestServer(array('require_exact_redirect_uri' => true)); + + // create a request with no "redirect_uri" in querystring + $request = new Request(array( + 'client_id' => 'Test Client ID with Redirect Uri Parts', // valid client id + 'response_type' => 'code', + 'redirect_uri' => 'http://user:pass@brentertainment.com:2222/authorize/cb?auth_type=oauth&test=true&hereisa=querystring', + )); + + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'redirect_uri_mismatch'); + $this->assertEquals($response->getParameter('error_description'), 'The redirect URI provided is missing or does not match'); + } + + public function testRedirectUriWithDifferentQueryAndExactMatchNotRequired() + { + $server = $this->getTestServer(array('require_exact_redirect_uri' => false)); + + // create a request with no "redirect_uri" in querystring + $request = new Request(array( + 'client_id' => 'Test Client ID with Redirect Uri Parts', // valid client id + 'response_type' => 'code', + 'redirect_uri' => 'http://user:pass@brentertainment.com:2222/authorize/cb?auth_type=oauth&test=true&hereisa=querystring', + 'state' => 'xyz', + )); + + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $this->assertContains('code', $response->getHttpHeader('Location')); + } + + public function testMultipleRedirectUris() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID with Multiple Redirect Uris', // valid client id + 'redirect_uri' => 'http://brentertainment.com', // valid redirect URI + 'response_type' => 'code', + 'state' => 'xyz' + )); + + $server->handleAuthorizeRequest($request, $response = new Response(), true); + $this->assertEquals($response->getStatusCode(), 302); + $this->assertContains('code', $response->getHttpHeader('Location')); + + // call again with different (but still valid) redirect URI + $request->query['redirect_uri'] = 'http://morehazards.com'; + + $server->handleAuthorizeRequest($request, $response = new Response(), true); + $this->assertEquals($response->getStatusCode(), 302); + $this->assertContains('code', $response->getHttpHeader('Location')); + } + + /** + * @see http://tools.ietf.org/html/rfc6749#section-4.1.3 + * @see https://github.com/bshaffer/oauth2-server-php/issues/163 + */ + public function testNoRedirectUriSuppliedDoesNotRequireTokenRedirectUri() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID with Redirect Uri', // valid client id + 'response_type' => 'code', + 'state' => 'xyz', + )); + + $server->handleAuthorizeRequest($request, $response = new Response(), true); + $this->assertEquals($response->getStatusCode(), 302); + $this->assertContains('state', $response->getHttpHeader('Location')); + $this->assertStringStartsWith('http://brentertainment.com?code=', $response->getHttpHeader('Location')); + + $parts = parse_url($response->getHttpHeader('Location')); + parse_str($parts['query'], $query); + + // call token endpoint with no redirect_uri supplied + $request = TestRequest::createPost(array( + 'client_id' => 'Test Client ID with Redirect Uri', // valid client id + 'client_secret' => 'TestSecret2', + 'grant_type' => 'authorization_code', + 'code' => $query['code'], + )); + + $server->handleTokenRequest($request, $response = new Response(), true); + $this->assertEquals($response->getStatusCode(), 200); + $this->assertNotNull($response->getParameter('access_token')); + } + + public function testUserDeniesAccessResponse() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'code', + 'state' => 'xyz', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), false); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + parse_str($parts['query'], $query); + + $this->assertEquals($query['error'], 'access_denied'); + $this->assertEquals($query['error_description'], 'The user denied access to your application'); + } + + public function testCodeQueryParamIsSet() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'code', + 'state' => 'xyz', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + parse_str($parts['query'], $query); + + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + + $this->assertEquals('http', $parts['scheme']); // same as passed in to redirect_uri + $this->assertEquals('adobe.com', $parts['host']); // same as passed in to redirect_uri + $this->assertArrayHasKey('query', $parts); + $this->assertFalse(isset($parts['fragment'])); + + // assert fragment is in "application/x-www-form-urlencoded" format + parse_str($parts['query'], $query); + $this->assertNotNull($query); + $this->assertArrayHasKey('code', $query); + + // ensure no id_token was saved, since the openid scope wasn't requested + $storage = $server->getStorage('authorization_code'); + $code = $storage->getAuthorizationCode($query['code']); + $this->assertTrue(empty($code['id_token'])); + + // ensure no error was returned + $this->assertFalse(isset($query['error'])); + $this->assertFalse(isset($query['error_description'])); + } + + public function testSuccessfulRequestReturnsStateParameter() + { + $server = $this->getTestServer(array('allow_implicit' => true)); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'code', + 'state' => 'test', // valid state string (just needs to be passed back to us) + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + $this->assertArrayHasKey('query', $parts); + parse_str($parts['query'], $query); + + $this->assertArrayHasKey('state', $query); + $this->assertEquals($query['state'], 'test'); + + // ensure no error was returned + $this->assertFalse(isset($query['error'])); + $this->assertFalse(isset($query['error_description'])); + } + + public function testSuccessfulRequestStripsExtraParameters() + { + $server = $this->getTestServer(array('allow_implicit' => true)); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'code', + 'state' => 'test', // valid state string (just needs to be passed back to us) + 'fake' => 'something', // extra query param + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $this->assertNotContains('error', $location); + + $parts = parse_url($location); + $this->assertFalse(isset($parts['fake'])); + $this->assertArrayHasKey('query', $parts); + parse_str($parts['query'], $query); + + $this->assertFalse(isset($parmas['fake'])); + $this->assertArrayHasKey('state', $query); + $this->assertEquals($query['state'], 'test'); + } + + public function testSuccessfulOpenidConnectRequest() + { + $server = $this->getTestServer(array( + 'use_openid_connect' => true, + 'issuer' => 'bojanz', + )); + + $request = new Request(array( + 'client_id' => 'Test Client ID', + 'redirect_uri' => 'http://adobe.com', + 'response_type' => 'code', + 'state' => 'xyz', + 'scope' => 'openid', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + parse_str($parts['query'], $query); + + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + $this->assertArrayHasKey('query', $parts); + $this->assertFalse(isset($parts['fragment'])); + + // assert fragment is in "application/x-www-form-urlencoded" format + parse_str($parts['query'], $query); + $this->assertNotNull($query); + $this->assertArrayHasKey('code', $query); + + // ensure no error was returned + $this->assertFalse(isset($query['error'])); + $this->assertFalse(isset($query['error_description'])); + + // confirm that the id_token has been created. + $storage = $server->getStorage('authorization_code'); + $code = $storage->getAuthorizationCode($query['code']); + $this->assertTrue(!empty($code['id_token'])); + } + + public function testCreateController() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $controller = new AuthorizeController($storage); + } + + private function getTestServer($config = array()) + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage, $config); + + // Add the two types supported for authorization grant + $server->addGrantType(new AuthorizationCode($storage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/ResourceControllerTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/ResourceControllerTest.php new file mode 100644 index 000000000..b277514a5 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/ResourceControllerTest.php @@ -0,0 +1,176 @@ +<?php + +namespace OAuth2\Controller; + +use OAuth2\Storage\Bootstrap; +use OAuth2\Server; +use OAuth2\GrantType\AuthorizationCode; +use OAuth2\Request; +use OAuth2\Response; + +class ResourceControllerTest extends \PHPUnit_Framework_TestCase +{ + public function testNoAccessToken() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertFalse($allow); + + $this->assertEquals($response->getStatusCode(), 401); + $this->assertNull($response->getParameter('error')); + $this->assertNull($response->getParameter('error_description')); + $this->assertEquals('', $response->getResponseBody()); + } + + public function testMalformedHeader() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->headers['AUTHORIZATION'] = 'tH1s i5 B0gU5'; + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertFalse($allow); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Malformed auth header'); + } + + public function testMultipleTokensSubmitted() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->request['access_token'] = 'TEST'; + $request->query['access_token'] = 'TEST'; + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertFalse($allow); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Only one method may be used to authenticate at a time (Auth header, GET or POST)'); + } + + public function testInvalidRequestMethod() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->server['REQUEST_METHOD'] = 'GET'; + $request->request['access_token'] = 'TEST'; + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertFalse($allow); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'When putting the token in the body, the method must be POST or PUT'); + } + + public function testInvalidContentType() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->server['REQUEST_METHOD'] = 'POST'; + $request->server['CONTENT_TYPE'] = 'application/json'; + $request->request['access_token'] = 'TEST'; + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertFalse($allow); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'The content type for POST requests must be "application/x-www-form-urlencoded"'); + } + + public function testInvalidToken() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->headers['AUTHORIZATION'] = 'Bearer TESTTOKEN'; + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertFalse($allow); + + $this->assertEquals($response->getStatusCode(), 401); + $this->assertEquals($response->getParameter('error'), 'invalid_token'); + $this->assertEquals($response->getParameter('error_description'), 'The access token provided is invalid'); + } + + public function testExpiredToken() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-expired'; + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertFalse($allow); + + $this->assertEquals($response->getStatusCode(), 401); + $this->assertEquals($response->getParameter('error'), 'invalid_token'); + $this->assertEquals($response->getParameter('error_description'), 'The access token provided has expired'); + } + + public function testOutOfScopeToken() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-scope'; + $scope = 'outofscope'; + $allow = $server->verifyResourceRequest($request, $response = new Response(), $scope); + $this->assertFalse($allow); + + $this->assertEquals($response->getStatusCode(), 403); + $this->assertEquals($response->getParameter('error'), 'insufficient_scope'); + $this->assertEquals($response->getParameter('error_description'), 'The request requires higher privileges than provided by the access token'); + + // verify the "scope" has been set in the "WWW-Authenticate" header + preg_match('/scope="(.*?)"/', $response->getHttpHeader('WWW-Authenticate'), $matches); + $this->assertEquals(2, count($matches)); + $this->assertEquals($matches[1], 'outofscope'); + } + + public function testMalformedToken() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-malformed'; + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertFalse($allow); + + $this->assertEquals($response->getStatusCode(), 401); + $this->assertEquals($response->getParameter('error'), 'malformed_token'); + $this->assertEquals($response->getParameter('error_description'), 'Malformed token (missing "expires")'); + } + + public function testValidToken() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-scope'; + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertTrue($allow); + } + + public function testValidTokenWithScopeParam() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-scope'; + $request->query['scope'] = 'testscope'; + $allow = $server->verifyResourceRequest($request, $response = new Response()); + $this->assertTrue($allow); + } + + public function testCreateController() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $tokenType = new \OAuth2\TokenType\Bearer(); + $controller = new ResourceController($tokenType, $storage); + } + + private function getTestServer($config = array()) + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage, $config); + + // Add the two types supported for authorization grant + $server->addGrantType(new AuthorizationCode($storage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/TokenControllerTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/TokenControllerTest.php new file mode 100644 index 000000000..4a217bd55 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/TokenControllerTest.php @@ -0,0 +1,289 @@ +<?php + +namespace OAuth2\Controller; + +use OAuth2\Storage\Bootstrap; +use OAuth2\Server; +use OAuth2\GrantType\AuthorizationCode; +use OAuth2\GrantType\ClientCredentials; +use OAuth2\GrantType\UserCredentials; +use OAuth2\Scope; +use OAuth2\Request\TestRequest; +use OAuth2\Response; + +class TokenControllerTest extends \PHPUnit_Framework_TestCase +{ + public function testNoGrantType() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $server->handleTokenRequest(TestRequest::createPost(), $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'The grant type was not specified in the request'); + } + + public function testInvalidGrantType() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'invalid_grant_type', // invalid grant type + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'unsupported_grant_type'); + $this->assertEquals($response->getParameter('error_description'), 'Grant type "invalid_grant_type" not supported'); + } + + public function testNoClientId() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'code' => 'testcode', + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_client'); + $this->assertEquals($response->getParameter('error_description'), 'Client credentials were not found in the headers or body'); + } + + public function testNoClientSecretWithConfidentialClient() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'code' => 'testcode', + 'client_id' => 'Test Client ID', // valid client id + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_client'); + $this->assertEquals($response->getParameter('error_description'), 'This client is invalid or must authenticate using a client secret'); + } + + public function testNoClientSecretWithEmptySecret() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'code' => 'testcode-empty-secret', + 'client_id' => 'Test Client ID Empty Secret', // valid client id + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 200); + } + + public function testInvalidClientId() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'code' => 'testcode', + 'client_id' => 'Fake Client ID', // invalid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_client'); + $this->assertEquals($response->getParameter('error_description'), 'The client credentials are invalid'); + } + + public function testInvalidClientSecret() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'code' => 'testcode', + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'Fake Client Secret', // invalid client secret + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_client'); + $this->assertEquals($response->getParameter('error_description'), 'The client credentials are invalid'); + } + + public function testValidTokenResponse() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode', // valid authorization code + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertTrue($response instanceof Response); + $this->assertEquals($response->getStatusCode(), 200); + $this->assertNull($response->getParameter('error')); + $this->assertNull($response->getParameter('error_description')); + $this->assertNotNull($response->getParameter('access_token')); + $this->assertNotNull($response->getParameter('expires_in')); + $this->assertNotNull($response->getParameter('token_type')); + } + + public function testValidClientIdScope() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'code' => 'testcode', + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'scope' => 'clientscope1 clientscope2' + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 200); + $this->assertNull($response->getParameter('error')); + $this->assertNull($response->getParameter('error_description')); + $this->assertEquals('clientscope1 clientscope2', $response->getParameter('scope')); + } + + public function testInvalidClientIdScope() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'code' => 'testcode-with-scope', + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'scope' => 'clientscope3' + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_scope'); + $this->assertEquals($response->getParameter('error_description'), 'The scope requested is invalid for this request'); + } + + public function testEnforceScope() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage); + $server->addGrantType(new ClientCredentials($storage)); + + $scope = new Scope(array( + 'default_scope' => false, + 'supported_scopes' => array('testscope') + )); + $server->setScopeUtil($scope); + + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $response = $server->handleTokenRequest($request); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_scope'); + $this->assertEquals($response->getParameter('error_description'), 'This application requires you specify a scope parameter'); + } + + public function testCanReceiveAccessTokenUsingPasswordGrantTypeWithoutClientSecret() + { + // add the test parameters in memory + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage); + $server->addGrantType(new UserCredentials($storage)); + + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID For Password Grant', // valid client id + 'username' => 'johndoe', // valid username + 'password' => 'password', // valid password for username + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertTrue($response instanceof Response); + $this->assertEquals(200, $response->getStatusCode(), var_export($response, 1)); + $this->assertNull($response->getParameter('error')); + $this->assertNull($response->getParameter('error_description')); + $this->assertNotNull($response->getParameter('access_token')); + $this->assertNotNull($response->getParameter('expires_in')); + $this->assertNotNull($response->getParameter('token_type')); + } + + public function testInvalidTokenTypeHintForRevoke() + { + $server = $this->getTestServer(); + + $request = TestRequest::createPost(array( + 'token_type_hint' => 'foo', + 'token' => 'sometoken' + )); + + $server->handleRevokeRequest($request, $response = new Response()); + + $this->assertTrue($response instanceof Response); + $this->assertEquals(400, $response->getStatusCode(), var_export($response, 1)); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Token type hint must be either \'access_token\' or \'refresh_token\''); + } + + public function testMissingTokenForRevoke() + { + $server = $this->getTestServer(); + + $request = TestRequest::createPost(array( + 'token_type_hint' => 'access_token' + )); + + $server->handleRevokeRequest($request, $response = new Response()); + $this->assertTrue($response instanceof Response); + $this->assertEquals(400, $response->getStatusCode(), var_export($response, 1)); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Missing token parameter to revoke'); + } + + public function testInvalidRequestMethodForRevoke() + { + $server = $this->getTestServer(); + + $request = new TestRequest(); + $request->setQuery(array( + 'token_type_hint' => 'access_token' + )); + + $server->handleRevokeRequest($request, $response = new Response()); + $this->assertTrue($response instanceof Response); + $this->assertEquals(405, $response->getStatusCode(), var_export($response, 1)); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'The request method must be POST when revoking an access token'); + } + + public function testCreateController() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $accessToken = new \OAuth2\ResponseType\AccessToken($storage); + $controller = new TokenController($accessToken, $storage); + } + + private function getTestServer() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage); + $server->addGrantType(new AuthorizationCode($storage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Encryption/FirebaseJwtTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Encryption/FirebaseJwtTest.php new file mode 100644 index 000000000..d34136767 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Encryption/FirebaseJwtTest.php @@ -0,0 +1,102 @@ +<?php + +namespace OAuth2\Encryption; + +use OAuth2\Storage\Bootstrap; + +class FirebaseJwtTest extends \PHPUnit_Framework_TestCase +{ + private $privateKey; + + public function setUp() + { + $this->privateKey = <<<EOD +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQC5/SxVlE8gnpFqCxgl2wjhzY7ucEi00s0kUg3xp7lVEvgLgYcA +nHiWp+gtSjOFfH2zsvpiWm6Lz5f743j/FEzHIO1owR0p4d9pOaJK07d01+RzoQLO +IQAgXrr4T1CCWUesncwwPBVCyy2Mw3Nmhmr9MrF8UlvdRKBxriRnlP3qJQIDAQAB +AoGAVgJJVU4fhYMu1e5JfYAcTGfF+Gf+h3iQm4JCpoUcxMXf5VpB9ztk3K7LRN5y +kwFuFALpnUAarRcUPs0D8FoP4qBluKksbAtgHkO7bMSH9emN+mH4le4qpFlR7+P1 +3fLE2Y19IBwPwEfClC+TpJvuog6xqUYGPlg6XLq/MxQUB4ECQQDgovP1v+ONSeGS +R+NgJTR47noTkQT3M2izlce/OG7a+O0yw6BOZjNXqH2wx3DshqMcPUFrTjibIClP +l/tEQ3ShAkEA0/TdBYDtXpNNjqg0R9GVH2pw7Kh68ne6mZTuj0kCgFYpUF6L6iMm +zXamIJ51rTDsTyKTAZ1JuAhAsK/M2BbDBQJAKQ5fXEkIA+i+64dsDUR/hKLBeRYG +PFAPENONQGvGBwt7/s02XV3cgGbxIgAxqWkqIp0neb9AJUoJgtyaNe3GQQJANoL4 +QQ0af0NVJAZgg8QEHTNL3aGrFSbzx8IE5Lb7PLRsJa5bP5lQxnDoYuU+EI/Phr62 +niisp/b/ZDGidkTMXQJBALeRsH1I+LmICAvWXpLKa9Gv0zGCwkuIJLiUbV9c6CVh +suocCAteQwL5iW2gA4AnYr5OGeHFsEl7NCQcwfPZpJ0= +-----END RSA PRIVATE KEY----- +EOD; + } + + /** @dataProvider provideClientCredentials */ + public function testJwtUtil($client_id, $client_key) + { + $jwtUtil = new FirebaseJwt(); + + $params = array( + 'iss' => $client_id, + 'exp' => time() + 1000, + 'iat' => time(), + 'sub' => 'testuser@ourdomain.com', + 'aud' => 'http://myapp.com/oauth/auth', + 'scope' => null, + ); + + $encoded = $jwtUtil->encode($params, $this->privateKey, 'RS256'); + + // test BC behaviour of trusting the algorithm in the header + $payload = $jwtUtil->decode($encoded, $client_key, array('RS256')); + $this->assertEquals($params, $payload); + + // test BC behaviour of not verifying by passing false + $payload = $jwtUtil->decode($encoded, $client_key, false); + $this->assertEquals($params, $payload); + + // test the new restricted algorithms header + $payload = $jwtUtil->decode($encoded, $client_key, array('RS256')); + $this->assertEquals($params, $payload); + } + + public function testInvalidJwt() + { + $jwtUtil = new FirebaseJwt(); + + $this->assertFalse($jwtUtil->decode('goob')); + $this->assertFalse($jwtUtil->decode('go.o.b')); + } + + /** @dataProvider provideClientCredentials */ + public function testInvalidJwtHeader($client_id, $client_key) + { + $jwtUtil = new FirebaseJwt(); + + $params = array( + 'iss' => $client_id, + 'exp' => time() + 1000, + 'iat' => time(), + 'sub' => 'testuser@ourdomain.com', + 'aud' => 'http://myapp.com/oauth/auth', + 'scope' => null, + ); + + // testing for algorithm tampering when only RSA256 signing is allowed + // @see https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/ + $tampered = $jwtUtil->encode($params, $client_key, 'HS256'); + + $payload = $jwtUtil->decode($tampered, $client_key, array('RS256')); + + $this->assertFalse($payload); + } + + public function provideClientCredentials() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $client_id = 'Test Client ID'; + $client_key = $storage->getClientKey($client_id, "testuser@ourdomain.com"); + + return array( + array($client_id, $client_key), + ); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Encryption/JwtTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Encryption/JwtTest.php new file mode 100644 index 000000000..214eebac8 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Encryption/JwtTest.php @@ -0,0 +1,102 @@ +<?php + +namespace OAuth2\Encryption; + +use OAuth2\Storage\Bootstrap; + +class JwtTest extends \PHPUnit_Framework_TestCase +{ + private $privateKey; + + public function setUp() + { + $this->privateKey = <<<EOD +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQC5/SxVlE8gnpFqCxgl2wjhzY7ucEi00s0kUg3xp7lVEvgLgYcA +nHiWp+gtSjOFfH2zsvpiWm6Lz5f743j/FEzHIO1owR0p4d9pOaJK07d01+RzoQLO +IQAgXrr4T1CCWUesncwwPBVCyy2Mw3Nmhmr9MrF8UlvdRKBxriRnlP3qJQIDAQAB +AoGAVgJJVU4fhYMu1e5JfYAcTGfF+Gf+h3iQm4JCpoUcxMXf5VpB9ztk3K7LRN5y +kwFuFALpnUAarRcUPs0D8FoP4qBluKksbAtgHkO7bMSH9emN+mH4le4qpFlR7+P1 +3fLE2Y19IBwPwEfClC+TpJvuog6xqUYGPlg6XLq/MxQUB4ECQQDgovP1v+ONSeGS +R+NgJTR47noTkQT3M2izlce/OG7a+O0yw6BOZjNXqH2wx3DshqMcPUFrTjibIClP +l/tEQ3ShAkEA0/TdBYDtXpNNjqg0R9GVH2pw7Kh68ne6mZTuj0kCgFYpUF6L6iMm +zXamIJ51rTDsTyKTAZ1JuAhAsK/M2BbDBQJAKQ5fXEkIA+i+64dsDUR/hKLBeRYG +PFAPENONQGvGBwt7/s02XV3cgGbxIgAxqWkqIp0neb9AJUoJgtyaNe3GQQJANoL4 +QQ0af0NVJAZgg8QEHTNL3aGrFSbzx8IE5Lb7PLRsJa5bP5lQxnDoYuU+EI/Phr62 +niisp/b/ZDGidkTMXQJBALeRsH1I+LmICAvWXpLKa9Gv0zGCwkuIJLiUbV9c6CVh +suocCAteQwL5iW2gA4AnYr5OGeHFsEl7NCQcwfPZpJ0= +-----END RSA PRIVATE KEY----- +EOD; + } + + /** @dataProvider provideClientCredentials */ + public function testJwtUtil($client_id, $client_key) + { + $jwtUtil = new Jwt(); + + $params = array( + 'iss' => $client_id, + 'exp' => time() + 1000, + 'iat' => time(), + 'sub' => 'testuser@ourdomain.com', + 'aud' => 'http://myapp.com/oauth/auth', + 'scope' => null, + ); + + $encoded = $jwtUtil->encode($params, $this->privateKey, 'RS256'); + + // test BC behaviour of trusting the algorithm in the header + $payload = $jwtUtil->decode($encoded, $client_key); + $this->assertEquals($params, $payload); + + // test BC behaviour of not verifying by passing false + $payload = $jwtUtil->decode($encoded, $client_key, false); + $this->assertEquals($params, $payload); + + // test the new restricted algorithms header + $payload = $jwtUtil->decode($encoded, $client_key, array('RS256')); + $this->assertEquals($params, $payload); + } + + public function testInvalidJwt() + { + $jwtUtil = new Jwt(); + + $this->assertFalse($jwtUtil->decode('goob')); + $this->assertFalse($jwtUtil->decode('go.o.b')); + } + + /** @dataProvider provideClientCredentials */ + public function testInvalidJwtHeader($client_id, $client_key) + { + $jwtUtil = new Jwt(); + + $params = array( + 'iss' => $client_id, + 'exp' => time() + 1000, + 'iat' => time(), + 'sub' => 'testuser@ourdomain.com', + 'aud' => 'http://myapp.com/oauth/auth', + 'scope' => null, + ); + + // testing for algorithm tampering when only RSA256 signing is allowed + // @see https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/ + $tampered = $jwtUtil->encode($params, $client_key, 'HS256'); + + $payload = $jwtUtil->decode($tampered, $client_key, array('RS256')); + + $this->assertFalse($payload); + } + + public function provideClientCredentials() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $client_id = 'Test Client ID'; + $client_key = $storage->getClientKey($client_id, "testuser@ourdomain.com"); + + return array( + array($client_id, $client_key), + ); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/AuthorizationCodeTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/AuthorizationCodeTest.php new file mode 100644 index 000000000..356b8e53c --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/AuthorizationCodeTest.php @@ -0,0 +1,223 @@ +<?php + +namespace OAuth2\GrantType; + +use OAuth2\Storage\Bootstrap; +use OAuth2\Server; +use OAuth2\Request\TestRequest; +use OAuth2\Response; + +class AuthorizationCodeTest extends \PHPUnit_Framework_TestCase +{ + public function testNoCode() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Missing parameter: "code" is required'); + } + + public function testInvalidCode() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'InvalidCode', // invalid authorization code + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Authorization code doesn\'t exist or is invalid for the client'); + } + + public function testCodeCannotBeUsedTwice() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode', // valid code + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 200); + $this->assertNotNull($response->getParameter('access_token')); + + // try to use the same code again + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Authorization code doesn\'t exist or is invalid for the client'); + } + + public function testExpiredCode() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode-expired', // expired authorization code + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'The authorization code has expired'); + } + + public function testValidCode() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode', // valid code + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + } + + public function testValidRedirectUri() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://brentertainment.com/voil%C3%A0', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode-redirect-uri', // valid code + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + } + + public function testValidCodeNoScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode-with-scope', // valid code + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('scope', $token); + $this->assertEquals($token['scope'], 'scope1 scope2'); + } + + public function testValidCodeSameScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode-with-scope', // valid code + 'scope' => 'scope2 scope1', + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('scope', $token); + $this->assertEquals($token['scope'], 'scope2 scope1'); + } + + public function testValidCodeLessScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode-with-scope', // valid code + 'scope' => 'scope1', + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('scope', $token); + $this->assertEquals($token['scope'], 'scope1'); + } + + public function testValidCodeDifferentScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode-with-scope', // valid code + 'scope' => 'scope3', + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_scope'); + $this->assertEquals($response->getParameter('error_description'), 'The scope requested is invalid for this request'); + } + + public function testValidCodeInvalidScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode-with-scope', // valid code + 'scope' => 'invalid-scope', + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_scope'); + $this->assertEquals($response->getParameter('error_description'), 'The scope requested is invalid for this request'); + } + + public function testValidClientDifferentCode() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Some Other Client', // valid client id + 'client_secret' => 'TestSecret3', // valid client secret + 'code' => 'testcode', // valid code + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'authorization_code doesn\'t exist or is invalid for the client'); + } + + private function getTestServer() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage); + $server->addGrantType(new AuthorizationCode($storage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/ClientCredentialsTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/ClientCredentialsTest.php new file mode 100644 index 000000000..f0d46ccb3 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/ClientCredentialsTest.php @@ -0,0 +1,159 @@ +<?php + +namespace OAuth2\GrantType; + +use OAuth2\Storage\Bootstrap; +use OAuth2\Server; +use OAuth2\Request\TestRequest; +use OAuth2\Request; +use OAuth2\Response; + +class ClientCredentialsTest extends \PHPUnit_Framework_TestCase +{ + public function testInvalidCredentials() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'FakeSecret', // valid client secret + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_client'); + $this->assertEquals($response->getParameter('error_description'), 'The client credentials are invalid'); + } + + public function testValidCredentials() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('scope', $token); + $this->assertNull($token['scope']); + } + + public function testValidCredentialsWithScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'scope' => 'scope1', + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('scope', $token); + $this->assertEquals($token['scope'], 'scope1'); + } + + public function testValidCredentialsInvalidScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'scope' => 'invalid-scope', + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_scope'); + $this->assertEquals($response->getParameter('error_description'), 'An unsupported scope was requested'); + } + + public function testValidCredentialsInHeader() + { + // create with HTTP_AUTHORIZATION in header + $server = $this->getTestServer(); + $headers = array('HTTP_AUTHORIZATION' => 'Basic '.base64_encode('Test Client ID:TestSecret'), 'REQUEST_METHOD' => 'POST'); + $params = array('grant_type' => 'client_credentials'); + $request = new Request(array(), $params, array(), array(), array(), $headers); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertNotNull($token['access_token']); + + // create using PHP Authorization Globals + $headers = array('PHP_AUTH_USER' => 'Test Client ID', 'PHP_AUTH_PW' => 'TestSecret', 'REQUEST_METHOD' => 'POST'); + $params = array('grant_type' => 'client_credentials'); + $request = new Request(array(), $params, array(), array(), array(), $headers); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertNotNull($token['access_token']); + } + + public function testValidCredentialsInRequest() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertNotNull($token['access_token']); + } + + public function testValidCredentialsInQuerystring() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertNotNull($token['access_token']); + } + + public function testClientUserIdIsSetInAccessToken() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Client ID With User ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + + // verify the user_id was associated with the token + $storage = $server->getStorage('client'); + $token = $storage->getAccessToken($token['access_token']); + $this->assertNotNull($token); + $this->assertArrayHasKey('user_id', $token); + $this->assertEquals($token['user_id'], 'brent@brentertainment.com'); + } + + private function getTestServer() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage); + $server->addGrantType(new ClientCredentials($storage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/ImplicitTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/ImplicitTest.php new file mode 100644 index 000000000..a47aae3e8 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/ImplicitTest.php @@ -0,0 +1,143 @@ +<?php + +namespace OAuth2\GrantType; + +use OAuth2\Storage\Bootstrap; +use OAuth2\Server; +use OAuth2\Request; +use OAuth2\Response; + +class ImplicitTest extends \PHPUnit_Framework_TestCase +{ + public function testImplicitNotAllowedResponse() + { + $server = $this->getTestServer(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'token', // invalid response type + )); + $server->handleAuthorizeRequest($request, $response = new Response(), false); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + parse_str($parts['query'], $query); + + $this->assertEquals($query['error'], 'unsupported_response_type'); + $this->assertEquals($query['error_description'], 'implicit grant type not supported'); + } + + public function testUserDeniesAccessResponse() + { + $server = $this->getTestServer(array('allow_implicit' => true)); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'token', // valid response type + 'state' => 'xyz', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), false); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + parse_str($parts['query'], $query); + + $this->assertEquals($query['error'], 'access_denied'); + $this->assertEquals($query['error_description'], 'The user denied access to your application'); + } + + public function testSuccessfulRequestFragmentParameter() + { + $server = $this->getTestServer(array('allow_implicit' => true)); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'token', // valid response type + 'state' => 'xyz', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $this->assertNull($response->getParameter('error')); + $this->assertNull($response->getParameter('error_description')); + + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + + $this->assertEquals('http', $parts['scheme']); // same as passed in to redirect_uri + $this->assertEquals('adobe.com', $parts['host']); // same as passed in to redirect_uri + $this->assertArrayHasKey('fragment', $parts); + $this->assertFalse(isset($parts['query'])); + + // assert fragment is in "application/x-www-form-urlencoded" format + parse_str($parts['fragment'], $params); + $this->assertNotNull($params); + $this->assertArrayHasKey('access_token', $params); + $this->assertArrayHasKey('expires_in', $params); + $this->assertArrayHasKey('token_type', $params); + } + + public function testSuccessfulRequestReturnsStateParameter() + { + $server = $this->getTestServer(array('allow_implicit' => true)); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'token', // valid response type + 'state' => 'test', // valid state string (just needs to be passed back to us) + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $this->assertNull($response->getParameter('error')); + $this->assertNull($response->getParameter('error_description')); + + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + $this->assertArrayHasKey('fragment', $parts); + parse_str($parts['fragment'], $params); + + $this->assertArrayHasKey('state', $params); + $this->assertEquals($params['state'], 'test'); + } + + public function testSuccessfulRequestStripsExtraParameters() + { + $server = $this->getTestServer(array('allow_implicit' => true)); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com?fake=something', // valid redirect URI + 'response_type' => 'token', // valid response type + 'state' => 'test', // valid state string (just needs to be passed back to us) + 'fake' => 'something', // add extra param to querystring + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $this->assertNull($response->getParameter('error')); + $this->assertNull($response->getParameter('error_description')); + + $location = $response->getHttpHeader('Location'); + $parts = parse_url($location); + $this->assertFalse(isset($parts['fake'])); + $this->assertArrayHasKey('fragment', $parts); + parse_str($parts['fragment'], $params); + + $this->assertFalse(isset($params['fake'])); + $this->assertArrayHasKey('state', $params); + $this->assertEquals($params['state'], 'test'); + } + + private function getTestServer($config = array()) + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage, $config); + + // Add the two types supported for authorization grant + $server->addGrantType(new AuthorizationCode($storage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/JwtBearerTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/JwtBearerTest.php new file mode 100644 index 000000000..0a6c4b827 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/JwtBearerTest.php @@ -0,0 +1,360 @@ +<?php + +namespace OAuth2\GrantType; + +use OAuth2\Storage\Bootstrap; +use OAuth2\Server; +use OAuth2\Request\TestRequest; +use OAuth2\Response; +use OAuth2\Encryption\Jwt; + +class JwtBearerTest extends \PHPUnit_Framework_TestCase +{ + private $privateKey; + + public function setUp() + { + $this->privateKey = <<<EOD +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQC5/SxVlE8gnpFqCxgl2wjhzY7ucEi00s0kUg3xp7lVEvgLgYcA +nHiWp+gtSjOFfH2zsvpiWm6Lz5f743j/FEzHIO1owR0p4d9pOaJK07d01+RzoQLO +IQAgXrr4T1CCWUesncwwPBVCyy2Mw3Nmhmr9MrF8UlvdRKBxriRnlP3qJQIDAQAB +AoGAVgJJVU4fhYMu1e5JfYAcTGfF+Gf+h3iQm4JCpoUcxMXf5VpB9ztk3K7LRN5y +kwFuFALpnUAarRcUPs0D8FoP4qBluKksbAtgHkO7bMSH9emN+mH4le4qpFlR7+P1 +3fLE2Y19IBwPwEfClC+TpJvuog6xqUYGPlg6XLq/MxQUB4ECQQDgovP1v+ONSeGS +R+NgJTR47noTkQT3M2izlce/OG7a+O0yw6BOZjNXqH2wx3DshqMcPUFrTjibIClP +l/tEQ3ShAkEA0/TdBYDtXpNNjqg0R9GVH2pw7Kh68ne6mZTuj0kCgFYpUF6L6iMm +zXamIJ51rTDsTyKTAZ1JuAhAsK/M2BbDBQJAKQ5fXEkIA+i+64dsDUR/hKLBeRYG +PFAPENONQGvGBwt7/s02XV3cgGbxIgAxqWkqIp0neb9AJUoJgtyaNe3GQQJANoL4 +QQ0af0NVJAZgg8QEHTNL3aGrFSbzx8IE5Lb7PLRsJa5bP5lQxnDoYuU+EI/Phr62 +niisp/b/ZDGidkTMXQJBALeRsH1I+LmICAvWXpLKa9Gv0zGCwkuIJLiUbV9c6CVh +suocCAteQwL5iW2gA4AnYr5OGeHFsEl7NCQcwfPZpJ0= +-----END RSA PRIVATE KEY----- +EOD; + } + + public function testMalformedJWT() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + )); + + //Get the jwt and break it + $jwt = $this->getJWT(); + $jwt = substr_replace($jwt, 'broken', 3, 6); + + $request->request['assertion'] = $jwt; + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'JWT is malformed'); + } + + public function testBrokenSignature() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + )); + + //Get the jwt and break signature + $jwt = $this->getJWT() . 'notSupposeToBeHere'; + $request->request['assertion'] = $jwt; + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'JWT failed signature verification'); + } + + public function testExpiredJWT() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + )); + + //Get an expired JWT + $jwt = $this->getJWT(1234); + $request->request['assertion'] = $jwt; + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'JWT has expired'); + } + + public function testBadExp() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + )); + + //Get an expired JWT + $jwt = $this->getJWT('badtimestamp'); + $request->request['assertion'] = $jwt; + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Expiration (exp) time must be a unix time stamp'); + } + + public function testNoAssert() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + )); + + //Do not pass the assert (JWT) + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Missing parameters: "assertion" required'); + } + + public function testNotBefore() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + )); + + //Get a future NBF + $jwt = $this->getJWT(null, time() + 10000); + $request->request['assertion'] = $jwt; + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'JWT cannot be used before the Not Before (nbf) time'); + } + + public function testBadNotBefore() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + )); + + //Get a non timestamp nbf + $jwt = $this->getJWT(null, 'notatimestamp'); + $request->request['assertion'] = $jwt; + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Not Before (nbf) time must be a unix time stamp'); + } + + public function testNonMatchingAudience() + { + $server = $this->getTestServer('http://google.com/oauth/o/auth'); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(), + )); + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Invalid audience (aud)'); + } + + public function testBadClientID() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(null, null, null, 'bad_client_id'), + )); + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Invalid issuer (iss) or subject (sub) provided'); + } + + public function testBadSubject() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(null, null, 'anotheruser@ourdomain,com'), + )); + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Invalid issuer (iss) or subject (sub) provided'); + } + + public function testMissingKey() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(null, null, null, 'Missing Key Cli,nt'), + )); + + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Invalid issuer (iss) or subject (sub) provided'); + } + + public function testValidJwt() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(), // valid assertion + )); + + $token = $server->grantAccessToken($request, new Response()); + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + } + + public function testValidJwtWithScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(null, null, null, 'Test Client ID'), // valid assertion + 'scope' => 'scope1', // valid scope + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('scope', $token); + $this->assertEquals($token['scope'], 'scope1'); + } + + public function testValidJwtInvalidScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(null, null, null, 'Test Client ID'), // valid assertion + 'scope' => 'invalid-scope', // invalid scope + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_scope'); + $this->assertEquals($response->getParameter('error_description'), 'An unsupported scope was requested'); + } + + public function testValidJti() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(null, null, 'testuser@ourdomain.com', 'Test Client ID', 'unused_jti'), // valid assertion with invalid scope + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + } + + public function testInvalidJti() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(99999999900, null, 'testuser@ourdomain.com', 'Test Client ID', 'used_jti'), // valid assertion with invalid scope + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'JSON Token Identifier (jti) has already been used'); + } + + public function testJtiReplayAttack() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type + 'assertion' => $this->getJWT(99999999900, null, 'testuser@ourdomain.com', 'Test Client ID', 'totally_new_jti'), // valid assertion with invalid scope + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + + //Replay the same request + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'JSON Token Identifier (jti) has already been used'); + } + + /** + * Generates a JWT + * @param $exp The expiration date. If the current time is greater than the exp, the JWT is invalid. + * @param $nbf The "not before" time. If the current time is less than the nbf, the JWT is invalid. + * @param $sub The subject we are acting on behalf of. This could be the email address of the user in the system. + * @param $iss The issuer, usually the client_id. + * @return string + */ + private function getJWT($exp = null, $nbf = null, $sub = null, $iss = 'Test Client ID', $jti = null) + { + if (!$exp) { + $exp = time() + 1000; + } + + if (!$sub) { + $sub = "testuser@ourdomain.com"; + } + + $params = array( + 'iss' => $iss, + 'exp' => $exp, + 'iat' => time(), + 'sub' => $sub, + 'aud' => 'http://myapp.com/oauth/auth', + ); + + if ($nbf) { + $params['nbf'] = $nbf; + } + + if ($jti) { + $params['jti'] = $jti; + } + + $jwtUtil = new Jwt(); + + return $jwtUtil->encode($params, $this->privateKey, 'RS256'); + } + + private function getTestServer($audience = 'http://myapp.com/oauth/auth') + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage); + $server->addGrantType(new JwtBearer($storage, $audience, new Jwt())); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/RefreshTokenTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/RefreshTokenTest.php new file mode 100644 index 000000000..a458aad8a --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/RefreshTokenTest.php @@ -0,0 +1,204 @@ +<?php + +namespace OAuth2\GrantType; + +use OAuth2\Storage\Bootstrap; +use OAuth2\Server; +use OAuth2\Request\TestRequest; +use OAuth2\Response; + +class RefreshTokenTest extends \PHPUnit_Framework_TestCase +{ + private $storage; + + public function testNoRefreshToken() + { + $server = $this->getTestServer(); + $server->addGrantType(new RefreshToken($this->storage)); + + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Missing parameter: "refresh_token" is required'); + } + + public function testInvalidRefreshToken() + { + $server = $this->getTestServer(); + $server->addGrantType(new RefreshToken($this->storage)); + + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => 'fake-token', // invalid refresh token + )); + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Invalid refresh token'); + } + + public function testValidRefreshTokenWithNewRefreshTokenInResponse() + { + $server = $this->getTestServer(); + $server->addGrantType(new RefreshToken($this->storage, array('always_issue_new_refresh_token' => true))); + + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => 'test-refreshtoken', // valid refresh token + )); + $token = $server->grantAccessToken($request, new Response()); + $this->assertTrue(isset($token['refresh_token']), 'refresh token should always refresh'); + + $refresh_token = $this->storage->getRefreshToken($token['refresh_token']); + $this->assertNotNull($refresh_token); + $this->assertEquals($refresh_token['refresh_token'], $token['refresh_token']); + $this->assertEquals($refresh_token['client_id'], $request->request('client_id')); + $this->assertTrue($token['refresh_token'] != 'test-refreshtoken', 'the refresh token returned is not the one used'); + $used_token = $this->storage->getRefreshToken('test-refreshtoken'); + $this->assertFalse($used_token, 'the refresh token used is no longer valid'); + } + + public function testValidRefreshTokenDoesNotUnsetToken() + { + $server = $this->getTestServer(); + $server->addGrantType(new RefreshToken($this->storage, array( + 'always_issue_new_refresh_token' => true, + 'unset_refresh_token_after_use' => false, + ))); + + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => 'test-refreshtoken', // valid refresh token + )); + $token = $server->grantAccessToken($request, new Response()); + $this->assertTrue(isset($token['refresh_token']), 'refresh token should always refresh'); + + $used_token = $this->storage->getRefreshToken('test-refreshtoken'); + $this->assertNotNull($used_token, 'the refresh token used is still valid'); + } + + public function testValidRefreshTokenWithNoRefreshTokenInResponse() + { + $server = $this->getTestServer(); + $server->addGrantType(new RefreshToken($this->storage, array('always_issue_new_refresh_token' => false))); + + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => 'test-refreshtoken', // valid refresh token + )); + $token = $server->grantAccessToken($request, new Response()); + $this->assertFalse(isset($token['refresh_token']), 'refresh token should not be returned'); + + $used_token = $this->storage->getRefreshToken('test-refreshtoken'); + $this->assertNotNull($used_token, 'the refresh token used is still valid'); + } + + public function testValidRefreshTokenSameScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => 'test-refreshtoken-with-scope', // valid refresh token (with scope) + 'scope' => 'scope2 scope1', + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('scope', $token); + $this->assertEquals($token['scope'], 'scope2 scope1'); + } + + public function testValidRefreshTokenLessScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => 'test-refreshtoken-with-scope', // valid refresh token (with scope) + 'scope' => 'scope1', + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('scope', $token); + $this->assertEquals($token['scope'], 'scope1'); + } + + public function testValidRefreshTokenDifferentScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => 'test-refreshtoken-with-scope', // valid refresh token (with scope) + 'scope' => 'scope3', + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_scope'); + $this->assertEquals($response->getParameter('error_description'), 'The scope requested is invalid for this request'); + } + + public function testValidRefreshTokenInvalidScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => 'test-refreshtoken-with-scope', // valid refresh token (with scope) + 'scope' => 'invalid-scope', + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_scope'); + $this->assertEquals($response->getParameter('error_description'), 'The scope requested is invalid for this request'); + } + + public function testValidClientDifferentRefreshToken() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Some Other Client', // valid client id + 'client_secret' => 'TestSecret3', // valid client secret + 'refresh_token' => 'test-refreshtoken', // valid refresh token + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'refresh_token doesn\'t exist or is invalid for the client'); + } + + private function getTestServer() + { + $this->storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($this->storage); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/UserCredentialsTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/UserCredentialsTest.php new file mode 100644 index 000000000..18943d055 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/UserCredentialsTest.php @@ -0,0 +1,172 @@ +<?php + +namespace OAuth2\GrantType; + +use OAuth2\Storage\Bootstrap; +use OAuth2\Server; +use OAuth2\Request\TestRequest; +use OAuth2\Response; + +class UserCredentialsTest extends \PHPUnit_Framework_TestCase +{ + public function testNoUsername() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'password' => 'testpass', // valid password + )); + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Missing parameters: "username" and "password" required'); + } + + public function testNoPassword() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'username' => 'test-username', // valid username + )); + $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'Missing parameters: "username" and "password" required'); + } + + public function testInvalidUsername() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'username' => 'fake-username', // valid username + 'password' => 'testpass', // valid password + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 401); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Invalid username and password combination'); + } + + public function testInvalidPassword() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'username' => 'test-username', // valid username + 'password' => 'fakepass', // invalid password + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 401); + $this->assertEquals($response->getParameter('error'), 'invalid_grant'); + $this->assertEquals($response->getParameter('error_description'), 'Invalid username and password combination'); + } + + public function testValidCredentials() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'username' => 'test-username', // valid username + 'password' => 'testpass', // valid password + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + } + + public function testValidCredentialsWithScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'username' => 'test-username', // valid username + 'password' => 'testpass', // valid password + 'scope' => 'scope1', // valid scope + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('scope', $token); + $this->assertEquals($token['scope'], 'scope1'); + } + + public function testValidCredentialsInvalidScope() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'username' => 'test-username', // valid username + 'password' => 'testpass', // valid password + 'scope' => 'invalid-scope', + )); + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_scope'); + $this->assertEquals($response->getParameter('error_description'), 'An unsupported scope was requested'); + } + + public function testNoSecretWithPublicClient() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID Empty Secret', // valid public client + 'username' => 'test-username', // valid username + 'password' => 'testpass', // valid password + )); + + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + } + + public function testNoSecretWithConfidentialClient() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID', // valid public client + 'username' => 'test-username', // valid username + 'password' => 'testpass', // valid password + )); + + $token = $server->grantAccessToken($request, $response = new Response()); + + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_client'); + $this->assertEquals($response->getParameter('error_description'), 'This client is invalid or must authenticate using a client secret'); + } + + private function getTestServer() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage); + $server->addGrantType(new UserCredentials($storage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php new file mode 100644 index 000000000..46de936d8 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php @@ -0,0 +1,182 @@ +<?php + +namespace OAuth2\OpenID\Controller; + +use OAuth2\Storage\Bootstrap; +use OAuth2\Server; +use OAuth2\Request; +use OAuth2\Response; + +class AuthorizeControllerTest extends \PHPUnit_Framework_TestCase +{ + public function testValidateAuthorizeRequest() + { + $server = $this->getTestServer(); + + $response = new Response(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'id_token', + 'state' => 'af0ifjsldkj', + 'nonce' => 'n-0S6_WzA2Mj', + )); + + // Test valid id_token request + $server->handleAuthorizeRequest($request, $response, true); + + $parts = parse_url($response->getHttpHeader('Location')); + parse_str($parts['fragment'], $query); + + $this->assertEquals('n-0S6_WzA2Mj', $server->getAuthorizeController()->getNonce()); + $this->assertEquals($query['state'], 'af0ifjsldkj'); + + $this->assertArrayHasKey('id_token', $query); + $this->assertArrayHasKey('state', $query); + $this->assertArrayNotHasKey('access_token', $query); + $this->assertArrayNotHasKey('expires_in', $query); + $this->assertArrayNotHasKey('token_type', $query); + + // Test valid token id_token request + $request->query['response_type'] = 'id_token token'; + $server->handleAuthorizeRequest($request, $response, true); + + $parts = parse_url($response->getHttpHeader('Location')); + parse_str($parts['fragment'], $query); + + $this->assertEquals('n-0S6_WzA2Mj', $server->getAuthorizeController()->getNonce()); + $this->assertEquals($query['state'], 'af0ifjsldkj'); + + $this->assertArrayHasKey('access_token', $query); + $this->assertArrayHasKey('expires_in', $query); + $this->assertArrayHasKey('token_type', $query); + $this->assertArrayHasKey('state', $query); + $this->assertArrayHasKey('id_token', $query); + + // assert that with multiple-valued response types, order does not matter + $request->query['response_type'] = 'token id_token'; + $server->handleAuthorizeRequest($request, $response, true); + + $parts = parse_url($response->getHttpHeader('Location')); + parse_str($parts['fragment'], $query); + + $this->assertEquals('n-0S6_WzA2Mj', $server->getAuthorizeController()->getNonce()); + $this->assertEquals($query['state'], 'af0ifjsldkj'); + + $this->assertArrayHasKey('access_token', $query); + $this->assertArrayHasKey('expires_in', $query); + $this->assertArrayHasKey('token_type', $query); + $this->assertArrayHasKey('state', $query); + $this->assertArrayHasKey('id_token', $query); + + // assert that with multiple-valued response types with extra spaces do not matter + $request->query['response_type'] = ' token id_token '; + $server->handleAuthorizeRequest($request, $response, true); + + $parts = parse_url($response->getHttpHeader('Location')); + parse_str($parts['fragment'], $query); + + $this->assertEquals('n-0S6_WzA2Mj', $server->getAuthorizeController()->getNonce()); + $this->assertEquals($query['state'], 'af0ifjsldkj'); + + $this->assertArrayHasKey('access_token', $query); + $this->assertArrayHasKey('expires_in', $query); + $this->assertArrayHasKey('token_type', $query); + $this->assertArrayHasKey('state', $query); + $this->assertArrayHasKey('id_token', $query); + } + + public function testMissingNonce() + { + $server = $this->getTestServer(); + $authorize = $server->getAuthorizeController(); + + $response = new Response(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'id_token', + 'state' => 'xyz', + )); + + // Test missing nonce for 'id_token' response type + $server->handleAuthorizeRequest($request, $response, true); + $params = $response->getParameters(); + + $this->assertEquals($params['error'], 'invalid_nonce'); + $this->assertEquals($params['error_description'], 'This application requires you specify a nonce parameter'); + + // Test missing nonce for 'id_token token' response type + $request->query['response_type'] = 'id_token token'; + $server->handleAuthorizeRequest($request, $response, true); + $params = $response->getParameters(); + + $this->assertEquals($params['error'], 'invalid_nonce'); + $this->assertEquals($params['error_description'], 'This application requires you specify a nonce parameter'); + } + + public function testNotGrantedApplication() + { + $server = $this->getTestServer(); + + $response = new Response(); + $request = new Request(array( + 'client_id' => 'Test Client ID', // valid client id + 'redirect_uri' => 'http://adobe.com', // valid redirect URI + 'response_type' => 'id_token', + 'state' => 'af0ifjsldkj', + 'nonce' => 'n-0S6_WzA2Mj', + )); + + // Test not approved application + $server->handleAuthorizeRequest($request, $response, false); + + $params = $response->getParameters(); + + $this->assertEquals($params['error'], 'consent_required'); + $this->assertEquals($params['error_description'], 'The user denied access to your application'); + + // Test not approved application with prompt parameter + $request->query['prompt'] = 'none'; + $server->handleAuthorizeRequest($request, $response, false); + + $params = $response->getParameters(); + + $this->assertEquals($params['error'], 'login_required'); + $this->assertEquals($params['error_description'], 'The user must log in'); + + // Test not approved application with user_id set + $request->query['prompt'] = 'none'; + $server->handleAuthorizeRequest($request, $response, false, 'some-user-id'); + + $params = $response->getParameters(); + + $this->assertEquals($params['error'], 'interaction_required'); + $this->assertEquals($params['error_description'], 'The user must grant access to your application'); + } + + public function testNeedsIdToken() + { + $server = $this->getTestServer(); + $authorize = $server->getAuthorizeController(); + + $this->assertTrue($authorize->needsIdToken('openid')); + $this->assertTrue($authorize->needsIdToken('openid profile')); + $this->assertFalse($authorize->needsIdToken('')); + $this->assertFalse($authorize->needsIdToken('some-scope')); + } + + private function getTestServer($config = array()) + { + $config += array( + 'use_openid_connect' => true, + 'issuer' => 'phpunit', + 'allow_implicit' => true + ); + + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage, $config); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php new file mode 100644 index 000000000..b1b687077 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php @@ -0,0 +1,44 @@ +<?php + +namespace OAuth2\OpenID\Controller; + +use OAuth2\Storage\Bootstrap; +use OAuth2\Server; +use OAuth2\Request; +use OAuth2\Response; + +class UserInfoControllerTest extends \PHPUnit_Framework_TestCase +{ + public function testCreateController() + { + $tokenType = new \OAuth2\TokenType\Bearer(); + $storage = new \OAuth2\Storage\Memory(); + $controller = new UserInfoController($tokenType, $storage, $storage); + + $response = new Response(); + $controller->handleUserInfoRequest(new Request(), $response); + $this->assertEquals(401, $response->getStatusCode()); + } + + public function testValidToken() + { + $server = $this->getTestServer(); + $request = Request::createFromGlobals(); + $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-openid-connect'; + $response = new Response(); + + $server->handleUserInfoRequest($request, $response); + $parameters = $response->getParameters(); + $this->assertEquals($parameters['sub'], 'testuser'); + $this->assertEquals($parameters['email'], 'testuser@test.com'); + $this->assertEquals($parameters['email_verified'], true); + } + + private function getTestServer($config = array()) + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage, $config); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/GrantType/AuthorizationCodeTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/GrantType/AuthorizationCodeTest.php new file mode 100644 index 000000000..776002d1e --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/GrantType/AuthorizationCodeTest.php @@ -0,0 +1,57 @@ +<?php + +namespace OAuth2\OpenID\GrantType; + +use OAuth2\Storage\Bootstrap; +use OAuth2\Server; +use OAuth2\Request\TestRequest; +use OAuth2\Response; + +class AuthorizationCodeTest extends \PHPUnit_Framework_TestCase +{ + public function testValidCode() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode-openid', // valid code + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('id_token', $token); + $this->assertEquals('test_id_token', $token['id_token']); + + // this is only true if "offline_access" was requested + $this->assertFalse(isset($token['refresh_token'])); + } + + public function testOfflineAccess() + { + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'code' => 'testcode-openid', // valid code + 'scope' => 'offline_access', // valid code + )); + $token = $server->grantAccessToken($request, new Response()); + + $this->assertNotNull($token); + $this->assertArrayHasKey('id_token', $token); + $this->assertEquals('test_id_token', $token['id_token']); + $this->assertTrue(isset($token['refresh_token'])); + } + + private function getTestServer() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage, array('use_openid_connect' => true)); + $server->addGrantType(new AuthorizationCode($storage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php new file mode 100644 index 000000000..b0311434a --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php @@ -0,0 +1,182 @@ +<?php + +namespace OAuth2\OpenID\ResponseType; + +use OAuth2\Server; +use OAuth2\Request; +use OAuth2\Response; +use OAuth2\Storage\Bootstrap; +use OAuth2\GrantType\ClientCredentials; + +class CodeIdTokenTest extends \PHPUnit_Framework_TestCase +{ + public function testHandleAuthorizeRequest() + { + // add the test parameters in memory + $server = $this->getTestServer(); + + $request = new Request(array( + 'response_type' => 'code id_token', + 'redirect_uri' => 'http://adobe.com', + 'client_id' => 'Test Client ID', + 'scope' => 'openid', + 'state' => 'test', + 'nonce' => 'test', + )); + + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $this->assertNotContains('error', $location); + + $parts = parse_url($location); + $this->assertArrayHasKey('query', $parts); + + // assert fragment is in "application/x-www-form-urlencoded" format + parse_str($parts['query'], $params); + $this->assertNotNull($params); + $this->assertArrayHasKey('id_token', $params); + $this->assertArrayHasKey('code', $params); + + // validate ID Token + $parts = explode('.', $params['id_token']); + foreach ($parts as &$part) { + // Each part is a base64url encoded json string. + $part = str_replace(array('-', '_'), array('+', '/'), $part); + $part = base64_decode($part); + $part = json_decode($part, true); + } + list($header, $claims, $signature) = $parts; + + $this->assertArrayHasKey('iss', $claims); + $this->assertArrayHasKey('sub', $claims); + $this->assertArrayHasKey('aud', $claims); + $this->assertArrayHasKey('iat', $claims); + $this->assertArrayHasKey('exp', $claims); + $this->assertArrayHasKey('auth_time', $claims); + $this->assertArrayHasKey('nonce', $claims); + + // only exists if an access token was granted along with the id_token + $this->assertArrayNotHasKey('at_hash', $claims); + + $this->assertEquals($claims['iss'], 'test'); + $this->assertEquals($claims['aud'], 'Test Client ID'); + $this->assertEquals($claims['nonce'], 'test'); + $duration = $claims['exp'] - $claims['iat']; + $this->assertEquals($duration, 3600); + } + + public function testUserClaimsWithUserId() + { + // add the test parameters in memory + $server = $this->getTestServer(); + + $request = new Request(array( + 'response_type' => 'code id_token', + 'redirect_uri' => 'http://adobe.com', + 'client_id' => 'Test Client ID', + 'scope' => 'openid email', + 'state' => 'test', + 'nonce' => 'test', + )); + + $userId = 'testuser'; + $server->handleAuthorizeRequest($request, $response = new Response(), true, $userId); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $this->assertNotContains('error', $location); + + $parts = parse_url($location); + $this->assertArrayHasKey('query', $parts); + + // assert fragment is in "application/x-www-form-urlencoded" format + parse_str($parts['query'], $params); + $this->assertNotNull($params); + $this->assertArrayHasKey('id_token', $params); + $this->assertArrayHasKey('code', $params); + + // validate ID Token + $parts = explode('.', $params['id_token']); + foreach ($parts as &$part) { + // Each part is a base64url encoded json string. + $part = str_replace(array('-', '_'), array('+', '/'), $part); + $part = base64_decode($part); + $part = json_decode($part, true); + } + list($header, $claims, $signature) = $parts; + + $this->assertArrayHasKey('email', $claims); + $this->assertArrayHasKey('email_verified', $claims); + $this->assertNotNull($claims['email']); + $this->assertNotNull($claims['email_verified']); + } + + public function testUserClaimsWithoutUserId() + { + // add the test parameters in memory + $server = $this->getTestServer(); + + $request = new Request(array( + 'response_type' => 'code id_token', + 'redirect_uri' => 'http://adobe.com', + 'client_id' => 'Test Client ID', + 'scope' => 'openid email', + 'state' => 'test', + 'nonce' => 'test', + )); + + $userId = null; + $server->handleAuthorizeRequest($request, $response = new Response(), true, $userId); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $this->assertNotContains('error', $location); + + $parts = parse_url($location); + $this->assertArrayHasKey('query', $parts); + + // assert fragment is in "application/x-www-form-urlencoded" format + parse_str($parts['query'], $params); + $this->assertNotNull($params); + $this->assertArrayHasKey('id_token', $params); + $this->assertArrayHasKey('code', $params); + + // validate ID Token + $parts = explode('.', $params['id_token']); + foreach ($parts as &$part) { + // Each part is a base64url encoded json string. + $part = str_replace(array('-', '_'), array('+', '/'), $part); + $part = base64_decode($part); + $part = json_decode($part, true); + } + list($header, $claims, $signature) = $parts; + + $this->assertArrayNotHasKey('email', $claims); + $this->assertArrayNotHasKey('email_verified', $claims); + } + + private function getTestServer($config = array()) + { + $config += array( + 'use_openid_connect' => true, + 'issuer' => 'test', + 'id_lifetime' => 3600, + 'allow_implicit' => true, + ); + + $memoryStorage = Bootstrap::getInstance()->getMemoryStorage(); + $memoryStorage->supportedScopes[] = 'email'; + $responseTypes = array( + 'code' => $code = new AuthorizationCode($memoryStorage), + 'id_token' => $idToken = new IdToken($memoryStorage, $memoryStorage, $config), + 'code id_token' => new CodeIdToken($code, $idToken), + ); + + $server = new Server($memoryStorage, $config, array(), $responseTypes); + $server->addGrantType(new ClientCredentials($memoryStorage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/IdTokenTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/IdTokenTest.php new file mode 100644 index 000000000..e772f6be4 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/IdTokenTest.php @@ -0,0 +1,184 @@ +<?php + +namespace OAuth2\OpenID\ResponseType; + +use OAuth2\Server; +use OAuth2\Request; +use OAuth2\Response; +use OAuth2\Storage\Bootstrap; +use OAuth2\GrantType\ClientCredentials; +use OAuth2\Encryption\Jwt; + +class IdTokenTest extends \PHPUnit_Framework_TestCase +{ + public function testValidateAuthorizeRequest() + { + $query = array( + 'response_type' => 'id_token', + 'redirect_uri' => 'http://adobe.com', + 'client_id' => 'Test Client ID', + 'scope' => 'openid', + 'state' => 'test', + ); + + // attempt to do the request without a nonce. + $server = $this->getTestServer(array('allow_implicit' => true)); + $request = new Request($query); + $valid = $server->validateAuthorizeRequest($request, $response = new Response()); + + // Add a nonce and retry. + $query['nonce'] = 'test'; + $request = new Request($query); + $valid = $server->validateAuthorizeRequest($request, $response = new Response()); + $this->assertTrue($valid); + } + + public function testHandleAuthorizeRequest() + { + // add the test parameters in memory + $server = $this->getTestServer(array('allow_implicit' => true)); + $request = new Request(array( + 'response_type' => 'id_token', + 'redirect_uri' => 'http://adobe.com', + 'client_id' => 'Test Client ID', + 'scope' => 'openid email', + 'state' => 'test', + 'nonce' => 'test', + )); + + $user_id = 'testuser'; + $server->handleAuthorizeRequest($request, $response = new Response(), true, $user_id); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $this->assertNotContains('error', $location); + + $parts = parse_url($location); + $this->assertArrayHasKey('fragment', $parts); + $this->assertFalse(isset($parts['query'])); + + // assert fragment is in "application/x-www-form-urlencoded" format + parse_str($parts['fragment'], $params); + $this->assertNotNull($params); + $this->assertArrayHasKey('id_token', $params); + $this->assertArrayNotHasKey('access_token', $params); + $this->validateIdToken($params['id_token']); + } + + public function testPassInAuthTime() + { + $server = $this->getTestServer(array('allow_implicit' => true)); + $request = new Request(array( + 'response_type' => 'id_token', + 'redirect_uri' => 'http://adobe.com', + 'client_id' => 'Test Client ID', + 'scope' => 'openid email', + 'state' => 'test', + 'nonce' => 'test', + )); + + // test with a scalar user id + $user_id = 'testuser123'; + $server->handleAuthorizeRequest($request, $response = new Response(), true, $user_id); + + list($header, $payload, $signature) = $this->extractTokenDataFromResponse($response); + + $this->assertTrue(is_array($payload)); + $this->assertArrayHasKey('sub', $payload); + $this->assertEquals($user_id, $payload['sub']); + $this->assertArrayHasKey('auth_time', $payload); + + // test with an array of user info + $userInfo = array( + 'user_id' => 'testuser1234', + 'auth_time' => date('Y-m-d H:i:s', strtotime('20 minutes ago') + )); + + $server->handleAuthorizeRequest($request, $response = new Response(), true, $userInfo); + + list($header, $payload, $signature) = $this->extractTokenDataFromResponse($response); + + $this->assertTrue(is_array($payload)); + $this->assertArrayHasKey('sub', $payload); + $this->assertEquals($userInfo['user_id'], $payload['sub']); + $this->assertArrayHasKey('auth_time', $payload); + $this->assertEquals($userInfo['auth_time'], $payload['auth_time']); + } + + private function extractTokenDataFromResponse(Response $response) + { + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $this->assertNotContains('error', $location); + + $parts = parse_url($location); + $this->assertArrayHasKey('fragment', $parts); + $this->assertFalse(isset($parts['query'])); + + parse_str($parts['fragment'], $params); + $this->assertNotNull($params); + $this->assertArrayHasKey('id_token', $params); + $this->assertArrayNotHasKey('access_token', $params); + + list($headb64, $payloadb64, $signature) = explode('.', $params['id_token']); + + $jwt = new Jwt(); + $header = json_decode($jwt->urlSafeB64Decode($headb64), true); + $payload = json_decode($jwt->urlSafeB64Decode($payloadb64), true); + + return array($header, $payload, $signature); + } + + private function validateIdToken($id_token) + { + $parts = explode('.', $id_token); + foreach ($parts as &$part) { + // Each part is a base64url encoded json string. + $part = str_replace(array('-', '_'), array('+', '/'), $part); + $part = base64_decode($part); + $part = json_decode($part, true); + } + list($header, $claims, $signature) = $parts; + + $this->assertArrayHasKey('iss', $claims); + $this->assertArrayHasKey('sub', $claims); + $this->assertArrayHasKey('aud', $claims); + $this->assertArrayHasKey('iat', $claims); + $this->assertArrayHasKey('exp', $claims); + $this->assertArrayHasKey('auth_time', $claims); + $this->assertArrayHasKey('nonce', $claims); + $this->assertArrayHasKey('email', $claims); + $this->assertArrayHasKey('email_verified', $claims); + + $this->assertEquals($claims['iss'], 'test'); + $this->assertEquals($claims['aud'], 'Test Client ID'); + $this->assertEquals($claims['nonce'], 'test'); + $this->assertEquals($claims['email'], 'testuser@test.com'); + $duration = $claims['exp'] - $claims['iat']; + $this->assertEquals($duration, 3600); + } + + private function getTestServer($config = array()) + { + $config += array( + 'use_openid_connect' => true, + 'issuer' => 'test', + 'id_lifetime' => 3600, + ); + + $memoryStorage = Bootstrap::getInstance()->getMemoryStorage(); + $memoryStorage->supportedScopes[] = 'email'; + $storage = array( + 'client' => $memoryStorage, + 'scope' => $memoryStorage, + ); + $responseTypes = array( + 'id_token' => new IdToken($memoryStorage, $memoryStorage, $config), + ); + + $server = new Server($storage, $config, array(), $responseTypes); + $server->addGrantType(new ClientCredentials($memoryStorage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php new file mode 100644 index 000000000..bc564d37b --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php @@ -0,0 +1,91 @@ +<?php + +namespace OAuth2\OpenID\ResponseType; + +use OAuth2\Server; +use OAuth2\Request; +use OAuth2\Response; +use OAuth2\Storage\Bootstrap; +use OAuth2\GrantType\ClientCredentials; +use OAuth2\ResponseType\AccessToken; + +class IdTokenTokenTest extends \PHPUnit_Framework_TestCase +{ + + public function testHandleAuthorizeRequest() + { + // add the test parameters in memory + $server = $this->getTestServer(array('allow_implicit' => true)); + + $request = new Request(array( + 'response_type' => 'id_token token', + 'redirect_uri' => 'http://adobe.com', + 'client_id' => 'Test Client ID', + 'scope' => 'openid', + 'state' => 'test', + 'nonce' => 'test', + )); + + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $this->assertNotContains('error', $location); + + $parts = parse_url($location); + $this->assertArrayHasKey('fragment', $parts); + $this->assertFalse(isset($parts['query'])); + + // assert fragment is in "application/x-www-form-urlencoded" format + parse_str($parts['fragment'], $params); + $this->assertNotNull($params); + $this->assertArrayHasKey('id_token', $params); + $this->assertArrayHasKey('access_token', $params); + + // validate ID Token + $parts = explode('.', $params['id_token']); + foreach ($parts as &$part) { + // Each part is a base64url encoded json string. + $part = str_replace(array('-', '_'), array('+', '/'), $part); + $part = base64_decode($part); + $part = json_decode($part, true); + } + list($header, $claims, $signature) = $parts; + + $this->assertArrayHasKey('iss', $claims); + $this->assertArrayHasKey('sub', $claims); + $this->assertArrayHasKey('aud', $claims); + $this->assertArrayHasKey('iat', $claims); + $this->assertArrayHasKey('exp', $claims); + $this->assertArrayHasKey('auth_time', $claims); + $this->assertArrayHasKey('nonce', $claims); + $this->assertArrayHasKey('at_hash', $claims); + + $this->assertEquals($claims['iss'], 'test'); + $this->assertEquals($claims['aud'], 'Test Client ID'); + $this->assertEquals($claims['nonce'], 'test'); + $duration = $claims['exp'] - $claims['iat']; + $this->assertEquals($duration, 3600); + } + + private function getTestServer($config = array()) + { + $config += array( + 'use_openid_connect' => true, + 'issuer' => 'test', + 'id_lifetime' => 3600, + ); + + $memoryStorage = Bootstrap::getInstance()->getMemoryStorage(); + $responseTypes = array( + 'token' => $token = new AccessToken($memoryStorage, $memoryStorage), + 'id_token' => $idToken = new IdToken($memoryStorage, $memoryStorage, $config), + 'id_token token' => new IdTokenToken($token, $idToken), + ); + + $server = new Server($memoryStorage, $config, array(), $responseTypes); + $server->addGrantType(new ClientCredentials($memoryStorage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Storage/AuthorizationCodeTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Storage/AuthorizationCodeTest.php new file mode 100644 index 000000000..bdfb085e3 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Storage/AuthorizationCodeTest.php @@ -0,0 +1,95 @@ +<?php + +namespace OAuth2\OpenID\Storage; + +use OAuth2\Storage\BaseTest; +use OAuth2\Storage\NullStorage; + +class AuthorizationCodeTest extends BaseTest +{ + /** @dataProvider provideStorage */ + public function testCreateAuthorizationCode($storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + if (!$storage instanceof AuthorizationCodeInterface) { + return; + } + + // assert code we are about to add does not exist + $code = $storage->getAuthorizationCode('new-openid-code'); + $this->assertFalse($code); + + // add new code + $expires = time() + 20; + $scope = null; + $id_token = 'fake_id_token'; + $success = $storage->setAuthorizationCode('new-openid-code', 'client ID', 'SOMEUSERID', 'http://example.com', $expires, $scope, $id_token); + $this->assertTrue($success); + + $code = $storage->getAuthorizationCode('new-openid-code'); + $this->assertNotNull($code); + $this->assertArrayHasKey('authorization_code', $code); + $this->assertArrayHasKey('client_id', $code); + $this->assertArrayHasKey('user_id', $code); + $this->assertArrayHasKey('redirect_uri', $code); + $this->assertArrayHasKey('expires', $code); + $this->assertEquals($code['authorization_code'], 'new-openid-code'); + $this->assertEquals($code['client_id'], 'client ID'); + $this->assertEquals($code['user_id'], 'SOMEUSERID'); + $this->assertEquals($code['redirect_uri'], 'http://example.com'); + $this->assertEquals($code['expires'], $expires); + $this->assertEquals($code['id_token'], $id_token); + + // change existing code + $expires = time() + 42; + $new_id_token = 'fake_id_token-2'; + $success = $storage->setAuthorizationCode('new-openid-code', 'client ID2', 'SOMEOTHERID', 'http://example.org', $expires, $scope, $new_id_token); + $this->assertTrue($success); + + $code = $storage->getAuthorizationCode('new-openid-code'); + $this->assertNotNull($code); + $this->assertArrayHasKey('authorization_code', $code); + $this->assertArrayHasKey('client_id', $code); + $this->assertArrayHasKey('user_id', $code); + $this->assertArrayHasKey('redirect_uri', $code); + $this->assertArrayHasKey('expires', $code); + $this->assertEquals($code['authorization_code'], 'new-openid-code'); + $this->assertEquals($code['client_id'], 'client ID2'); + $this->assertEquals($code['user_id'], 'SOMEOTHERID'); + $this->assertEquals($code['redirect_uri'], 'http://example.org'); + $this->assertEquals($code['expires'], $expires); + $this->assertEquals($code['id_token'], $new_id_token); + } + + /** @dataProvider provideStorage */ + public function testRemoveIdTokenFromAuthorizationCode($storage) + { + // add new code + $expires = time() + 20; + $scope = null; + $id_token = 'fake_id_token_to_remove'; + $authcode = 'new-openid-code-'.rand(); + $success = $storage->setAuthorizationCode($authcode, 'client ID', 'SOMEUSERID', 'http://example.com', $expires, $scope, $id_token); + $this->assertTrue($success); + + // verify params were set + $code = $storage->getAuthorizationCode($authcode); + $this->assertNotNull($code); + $this->assertArrayHasKey('id_token', $code); + $this->assertEquals($code['id_token'], $id_token); + + // remove the id_token + $success = $storage->setAuthorizationCode($authcode, 'client ID', 'SOMEUSERID', 'http://example.com', $expires, $scope, null); + + // verify the "id_token" is now null + $code = $storage->getAuthorizationCode($authcode); + $this->assertNotNull($code); + $this->assertArrayHasKey('id_token', $code); + $this->assertEquals($code['id_token'], null); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Storage/UserClaimsTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Storage/UserClaimsTest.php new file mode 100644 index 000000000..840f6c566 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Storage/UserClaimsTest.php @@ -0,0 +1,41 @@ +<?php + +namespace OAuth2\OpenID\Storage; + +use OAuth2\Storage\BaseTest; +use OAuth2\Storage\NullStorage; + +class UserClaimsTest extends BaseTest +{ + /** @dataProvider provideStorage */ + public function testGetUserClaims($storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + if (!$storage instanceof UserClaimsInterface) { + // incompatible storage + return; + } + + // invalid user + $claims = $storage->getUserClaims('fake-user', ''); + $this->assertFalse($claims); + + // valid user (no scope) + $claims = $storage->getUserClaims('testuser', ''); + + /* assert the decoded token is the same */ + $this->assertFalse(isset($claims['email'])); + + // valid user + $claims = $storage->getUserClaims('testuser', 'email'); + + /* assert the decoded token is the same */ + $this->assertEquals($claims['email'], "testuser@test.com"); + $this->assertEquals($claims['email_verified'], true); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/RequestTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/RequestTest.php new file mode 100644 index 000000000..10db3215c --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/RequestTest.php @@ -0,0 +1,98 @@ +<?php + +namespace OAuth2; + +use OAuth2\Request\TestRequest; +use OAuth2\Storage\Bootstrap; +use OAuth2\GrantType\AuthorizationCode; + +class RequestTest extends \PHPUnit_Framework_TestCase +{ + public function testRequestOverride() + { + $request = new TestRequest(); + $server = $this->getTestServer(); + + // Smoke test for override request class + // $server->handleTokenRequest($request, $response = new Response()); + // $this->assertInstanceOf('Response', $response); + // $server->handleAuthorizeRequest($request, $response = new Response(), true); + // $this->assertInstanceOf('Response', $response); + // $response = $server->verifyResourceRequest($request, $response = new Response()); + // $this->assertTrue(is_bool($response)); + + /*** make some valid requests ***/ + + // Valid Token Request + $request->setPost(array( + 'grant_type' => 'authorization_code', + 'client_id' => 'Test Client ID', + 'client_secret' => 'TestSecret', + 'code' => 'testcode', + )); + $server->handleTokenRequest($request, $response = new Response()); + $this->assertEquals($response->getStatusCode(), 200); + $this->assertNull($response->getParameter('error')); + $this->assertNotNUll($response->getParameter('access_token')); + } + + public function testHeadersReturnsValueByKey() + { + $request = new Request( + array(), + array(), + array(), + array(), + array(), + array(), + array(), + array('AUTHORIZATION' => 'Basic secret') + ); + + $this->assertEquals('Basic secret', $request->headers('AUTHORIZATION')); + } + + public function testHeadersReturnsDefaultIfHeaderNotPresent() + { + $request = new Request(); + + $this->assertEquals('Bearer', $request->headers('AUTHORIZATION', 'Bearer')); + } + + public function testHeadersIsCaseInsensitive() + { + $request = new Request( + array(), + array(), + array(), + array(), + array(), + array(), + array(), + array('AUTHORIZATION' => 'Basic secret') + ); + + $this->assertEquals('Basic secret', $request->headers('Authorization')); + } + + public function testRequestReturnsPostParamIfNoQueryParamAvailable() + { + $request = new Request( + array(), + array('client_id' => 'correct') + ); + + $this->assertEquals('correct', $request->query('client_id', $request->request('client_id'))); + } + + private function getTestServer($config = array()) + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage, $config); + + // Add the two types supported for authorization grant + $server->addGrantType(new AuthorizationCode($storage)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseTest.php new file mode 100644 index 000000000..b8149005d --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseTest.php @@ -0,0 +1,17 @@ +<?php + +namespace OAuth2; + +class ResponseTest extends \PHPUnit_Framework_TestCase +{ + public function testRenderAsXml() + { + $response = new Response(array( + 'foo' => 'bar', + 'halland' => 'oates', + )); + + $string = $response->getResponseBody('xml'); + $this->assertContains('<response><foo>bar</foo><halland>oates</halland></response>', $string); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseType/AccessTokenTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseType/AccessTokenTest.php new file mode 100644 index 000000000..0ed1c82fc --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseType/AccessTokenTest.php @@ -0,0 +1,107 @@ +<?php + +namespace OAuth2\ResponseType; + +use OAuth2\Server; +use OAuth2\Storage\Memory; + +class AccessTokenTest extends \PHPUnit_Framework_TestCase +{ + public function testRevokeAccessTokenWithTypeHint() + { + $tokenStorage = new Memory(array( + 'access_tokens' => array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke')); + $accessToken = new AccessToken($tokenStorage); + $accessToken->revokeToken('revoke', 'access_token'); + $this->assertFalse($tokenStorage->getAccessToken('revoke')); + } + + public function testRevokeAccessTokenWithoutTypeHint() + { + $tokenStorage = new Memory(array( + 'access_tokens' => array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke')); + $accessToken = new AccessToken($tokenStorage); + $accessToken->revokeToken('revoke'); + $this->assertFalse($tokenStorage->getAccessToken('revoke')); + } + + public function testRevokeRefreshTokenWithTypeHint() + { + $tokenStorage = new Memory(array( + 'refresh_tokens' => array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getRefreshToken('revoke')); + $accessToken = new AccessToken(new Memory, $tokenStorage); + $accessToken->revokeToken('revoke', 'refresh_token'); + $this->assertFalse($tokenStorage->getRefreshToken('revoke')); + } + + public function testRevokeRefreshTokenWithoutTypeHint() + { + $tokenStorage = new Memory(array( + 'refresh_tokens' => array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getRefreshToken('revoke')); + $accessToken = new AccessToken(new Memory, $tokenStorage); + $accessToken->revokeToken('revoke'); + $this->assertFalse($tokenStorage->getRefreshToken('revoke')); + } + + public function testRevokeAccessTokenWithRefreshTokenTypeHint() + { + $tokenStorage = new Memory(array( + 'access_tokens' => array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke')); + $accessToken = new AccessToken($tokenStorage); + $accessToken->revokeToken('revoke', 'refresh_token'); + $this->assertFalse($tokenStorage->getAccessToken('revoke')); + } + + public function testRevokeAccessTokenWithBogusTypeHint() + { + $tokenStorage = new Memory(array( + 'access_tokens' => array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke')); + $accessToken = new AccessToken($tokenStorage); + $accessToken->revokeToken('revoke', 'foo'); + $this->assertFalse($tokenStorage->getAccessToken('revoke')); + } + + public function testRevokeRefreshTokenWithBogusTypeHint() + { + $tokenStorage = new Memory(array( + 'refresh_tokens' => array( + 'revoke' => array('mytoken'), + ), + )); + + $this->assertEquals(array('mytoken'), $tokenStorage->getRefreshToken('revoke')); + $accessToken = new AccessToken(new Memory, $tokenStorage); + $accessToken->revokeToken('revoke', 'foo'); + $this->assertFalse($tokenStorage->getRefreshToken('revoke')); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseType/JwtAccessTokenTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseType/JwtAccessTokenTest.php new file mode 100644 index 000000000..51b01a927 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ResponseType/JwtAccessTokenTest.php @@ -0,0 +1,160 @@ +<?php + +namespace OAuth2\ResponseType; + +use OAuth2\Server; +use OAuth2\Response; +use OAuth2\Request\TestRequest; +use OAuth2\Storage\Bootstrap; +use OAuth2\Storage\JwtAccessToken as JwtAccessTokenStorage; +use OAuth2\GrantType\ClientCredentials; +use OAuth2\GrantType\UserCredentials; +use OAuth2\GrantType\RefreshToken; +use OAuth2\Encryption\Jwt; + +class JwtAccessTokenTest extends \PHPUnit_Framework_TestCase +{ + public function testCreateAccessToken() + { + $server = $this->getTestServer(); + $jwtResponseType = $server->getResponseType('token'); + + $accessToken = $jwtResponseType->createAccessToken('Test Client ID', 123, 'test', false); + $jwt = new Jwt; + $decodedAccessToken = $jwt->decode($accessToken['access_token'], null, false); + + $this->assertArrayHasKey('id', $decodedAccessToken); + $this->assertArrayHasKey('jti', $decodedAccessToken); + $this->assertArrayHasKey('iss', $decodedAccessToken); + $this->assertArrayHasKey('aud', $decodedAccessToken); + $this->assertArrayHasKey('exp', $decodedAccessToken); + $this->assertArrayHasKey('iat', $decodedAccessToken); + $this->assertArrayHasKey('token_type', $decodedAccessToken); + $this->assertArrayHasKey('scope', $decodedAccessToken); + + $this->assertEquals('https://api.example.com', $decodedAccessToken['iss']); + $this->assertEquals('Test Client ID', $decodedAccessToken['aud']); + $this->assertEquals(123, $decodedAccessToken['sub']); + $delta = $decodedAccessToken['exp'] - $decodedAccessToken['iat']; + $this->assertEquals(3600, $delta); + $this->assertEquals($decodedAccessToken['id'], $decodedAccessToken['jti']); + } + + public function testGrantJwtAccessToken() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $server->handleTokenRequest($request, $response = new Response()); + + $this->assertNotNull($response->getParameter('access_token')); + $this->assertEquals(2, substr_count($response->getParameter('access_token'), '.')); + } + + public function testAccessResourceWithJwtAccessToken() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $server->handleTokenRequest($request, $response = new Response()); + $this->assertNotNull($JwtAccessToken = $response->getParameter('access_token')); + + // make a call to the resource server using the crypto token + $request = TestRequest::createPost(array( + 'access_token' => $JwtAccessToken, + )); + + $this->assertTrue($server->verifyResourceRequest($request)); + } + + public function testAccessResourceWithJwtAccessTokenUsingSecondaryStorage() + { + // add the test parameters in memory + $server = $this->getTestServer(); + $request = TestRequest::createPost(array( + 'grant_type' => 'client_credentials', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + )); + $server->handleTokenRequest($request, $response = new Response()); + $this->assertNotNull($JwtAccessToken = $response->getParameter('access_token')); + + // make a call to the resource server using the crypto token + $request = TestRequest::createPost(array( + 'access_token' => $JwtAccessToken, + )); + + // create a resource server with the "memory" storage from the grant server + $resourceServer = new Server($server->getStorage('client_credentials')); + + $this->assertTrue($resourceServer->verifyResourceRequest($request)); + } + + public function testJwtAccessTokenWithRefreshToken() + { + $server = $this->getTestServer(); + + // add "UserCredentials" grant type and "JwtAccessToken" response type + // and ensure "JwtAccessToken" response type has "RefreshToken" storage + $memoryStorage = Bootstrap::getInstance()->getMemoryStorage(); + $server->addGrantType(new UserCredentials($memoryStorage)); + $server->addGrantType(new RefreshToken($memoryStorage)); + $server->addResponseType(new JwtAccessToken($memoryStorage, $memoryStorage, $memoryStorage), 'token'); + + $request = TestRequest::createPost(array( + 'grant_type' => 'password', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'username' => 'test-username', // valid username + 'password' => 'testpass', // valid password + )); + + // make the call to grant a crypto token + $server->handleTokenRequest($request, $response = new Response()); + $this->assertNotNull($JwtAccessToken = $response->getParameter('access_token')); + $this->assertNotNull($refreshToken = $response->getParameter('refresh_token')); + + // decode token and make sure refresh_token isn't set + list($header, $payload, $signature) = explode('.', $JwtAccessToken); + $decodedToken = json_decode(base64_decode($payload), true); + $this->assertFalse(array_key_exists('refresh_token', $decodedToken)); + + // use the refresh token to get another access token + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => $refreshToken, + )); + + $server->handleTokenRequest($request, $response = new Response()); + $this->assertNotNull($response->getParameter('access_token')); + } + + private function getTestServer() + { + $memoryStorage = Bootstrap::getInstance()->getMemoryStorage(); + + $storage = array( + 'access_token' => new JwtAccessTokenStorage($memoryStorage), + 'client' => $memoryStorage, + 'client_credentials' => $memoryStorage, + ); + $server = new Server($storage); + $server->addGrantType(new ClientCredentials($memoryStorage)); + + // make the "token" response type a JwtAccessToken + $config = array('issuer' => 'https://api.example.com'); + $server->addResponseType(new JwtAccessToken($memoryStorage, $memoryStorage, null, $config)); + + return $server; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/ScopeTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ScopeTest.php new file mode 100644 index 000000000..99f9cf6eb --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ScopeTest.php @@ -0,0 +1,42 @@ +<?php + +namespace OAuth2; + +use OAuth2\Storage\Memory; + +class ScopeTest extends \PHPUnit_Framework_TestCase +{ + public function testCheckScope() + { + $scopeUtil = new Scope(); + + $this->assertFalse($scopeUtil->checkScope('invalid', 'list of scopes')); + $this->assertTrue($scopeUtil->checkScope('valid', 'valid and-some other-scopes')); + $this->assertTrue($scopeUtil->checkScope('valid another-valid', 'valid another-valid and-some other-scopes')); + // all scopes must match + $this->assertFalse($scopeUtil->checkScope('valid invalid', 'valid and-some other-scopes')); + $this->assertFalse($scopeUtil->checkScope('valid valid2 invalid', 'valid valid2 and-some other-scopes')); + } + + public function testScopeStorage() + { + $scopeUtil = new Scope(); + $this->assertEquals($scopeUtil->getDefaultScope(), null); + + $scopeUtil = new Scope(array( + 'default_scope' => 'default', + 'supported_scopes' => array('this', 'that', 'another'), + )); + $this->assertEquals($scopeUtil->getDefaultScope(), 'default'); + $this->assertTrue($scopeUtil->scopeExists('this that another', 'client_id')); + + $memoryStorage = new Memory(array( + 'default_scope' => 'base', + 'supported_scopes' => array('only-this-one'), + )); + $scopeUtil = new Scope($memoryStorage); + + $this->assertEquals($scopeUtil->getDefaultScope(), 'base'); + $this->assertTrue($scopeUtil->scopeExists('only-this-one', 'client_id')); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/ServerTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ServerTest.php new file mode 100644 index 000000000..747e120f5 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/ServerTest.php @@ -0,0 +1,684 @@ +<?php + +namespace OAuth2; + +use OAuth2\Request\TestRequest; +use OAuth2\ResponseType\AuthorizationCode; +use OAuth2\Storage\Bootstrap; + +class ServerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @expectedException LogicException OAuth2\Storage\ClientInterface + **/ + public function testGetAuthorizeControllerWithNoClientStorageThrowsException() + { + // must set Client Storage + $server = new Server(); + $server->getAuthorizeController(); + } + + /** + * @expectedException LogicException OAuth2\Storage\AccessTokenInterface + **/ + public function testGetAuthorizeControllerWithNoAccessTokenStorageThrowsException() + { + // must set AccessToken or AuthorizationCode + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\ClientInterface')); + $server->getAuthorizeController(); + } + + public function testGetAuthorizeControllerWithClientStorageAndAccessTokenResponseType() + { + // must set AccessToken or AuthorizationCode + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\ClientInterface')); + $server->addResponseType($this->getMock('OAuth2\ResponseType\AccessTokenInterface')); + + $this->assertNotNull($server->getAuthorizeController()); + } + + public function testGetAuthorizeControllerWithClientStorageAndAuthorizationCodeResponseType() + { + // must set AccessToken or AuthorizationCode + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\ClientInterface')); + $server->addResponseType($this->getMock('OAuth2\ResponseType\AuthorizationCodeInterface')); + + $this->assertNotNull($server->getAuthorizeController()); + } + + /** + * @expectedException LogicException allow_implicit + **/ + public function testGetAuthorizeControllerWithClientStorageAndAccessTokenStorageThrowsException() + { + // must set AuthorizationCode or AccessToken / implicit + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\ClientInterface')); + $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface')); + + $this->assertNotNull($server->getAuthorizeController()); + } + + public function testGetAuthorizeControllerWithClientStorageAndAccessTokenStorage() + { + // must set AuthorizationCode or AccessToken / implicit + $server = new Server(array(), array('allow_implicit' => true)); + $server->addStorage($this->getMock('OAuth2\Storage\ClientInterface')); + $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface')); + + $this->assertNotNull($server->getAuthorizeController()); + } + + public function testGetAuthorizeControllerWithClientStorageAndAuthorizationCodeStorage() + { + // must set AccessToken or AuthorizationCode + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\ClientInterface')); + $server->addStorage($this->getMock('OAuth2\Storage\AuthorizationCodeInterface')); + + $this->assertNotNull($server->getAuthorizeController()); + } + + /** + * @expectedException LogicException grant_types + **/ + public function testGetTokenControllerWithGrantTypeStorageThrowsException() + { + $server = new Server(); + $server->getTokenController(); + } + + /** + * @expectedException LogicException OAuth2\Storage\ClientCredentialsInterface + **/ + public function testGetTokenControllerWithNoClientCredentialsStorageThrowsException() + { + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\UserCredentialsInterface')); + $server->getTokenController(); + } + + /** + * @expectedException LogicException OAuth2\Storage\AccessTokenInterface + **/ + public function testGetTokenControllerWithNoAccessTokenStorageThrowsException() + { + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface')); + $server->getTokenController(); + } + + public function testGetTokenControllerWithAccessTokenAndClientCredentialsStorage() + { + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface')); + $server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface')); + $server->getTokenController(); + } + + public function testGetTokenControllerAccessTokenStorageAndClientCredentialsStorageAndGrantTypes() + { + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface')); + $server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface')); + $server->addGrantType($this->getMockBuilder('OAuth2\GrantType\AuthorizationCode')->disableOriginalConstructor()->getMock()); + $server->getTokenController(); + } + + /** + * @expectedException LogicException OAuth2\Storage\AccessTokenInterface + **/ + public function testGetResourceControllerWithNoAccessTokenStorageThrowsException() + { + $server = new Server(); + $server->getResourceController(); + } + + public function testGetResourceControllerWithAccessTokenStorage() + { + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface')); + $server->getResourceController(); + } + + /** + * @expectedException InvalidArgumentException OAuth2\Storage\AccessTokenInterface + **/ + public function testAddingStorageWithInvalidClass() + { + $server = new Server(); + $server->addStorage(new \StdClass()); + } + + /** + * @expectedException InvalidArgumentException access_token + **/ + public function testAddingStorageWithInvalidKey() + { + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface'), 'nonexistant_storage'); + } + + /** + * @expectedException InvalidArgumentException OAuth2\Storage\AuthorizationCodeInterface + **/ + public function testAddingStorageWithInvalidKeyStorageCombination() + { + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface'), 'authorization_code'); + } + + public function testAddingStorageWithValidKeyOnlySetsThatKey() + { + $server = new Server(); + $server->addStorage($this->getMock('OAuth2\Storage\Memory'), 'access_token'); + + $reflection = new \ReflectionClass($server); + $prop = $reflection->getProperty('storages'); + $prop->setAccessible(true); + + $storages = $prop->getValue($server); // get the private "storages" property + + $this->assertEquals(1, count($storages)); + $this->assertTrue(isset($storages['access_token'])); + $this->assertFalse(isset($storages['authorization_code'])); + } + + public function testAddingClientStorageSetsClientCredentialsStorageByDefault() + { + $server = new Server(); + $memory = $this->getMock('OAuth2\Storage\Memory'); + $server->addStorage($memory, 'client'); + + $client_credentials = $server->getStorage('client_credentials'); + + $this->assertNotNull($client_credentials); + $this->assertEquals($client_credentials, $memory); + } + + public function testAddStorageWithNullValue() + { + $memory = $this->getMock('OAuth2\Storage\Memory'); + $server = new Server($memory); + $server->addStorage(null, 'refresh_token'); + + $client_credentials = $server->getStorage('client_credentials'); + + $this->assertNotNull($client_credentials); + $this->assertEquals($client_credentials, $memory); + + $refresh_token = $server->getStorage('refresh_token'); + + $this->assertNull($refresh_token); + } + + public function testNewServerWithNullStorageValue() + { + $memory = $this->getMock('OAuth2\Storage\Memory'); + $server = new Server(array( + 'client_credentials' => $memory, + 'refresh_token' => null, + )); + + $client_credentials = $server->getStorage('client_credentials'); + + $this->assertNotNull($client_credentials); + $this->assertEquals($client_credentials, $memory); + + $refresh_token = $server->getStorage('refresh_token'); + + $this->assertNull($refresh_token); + } + + public function testAddingClientCredentialsStorageSetsClientStorageByDefault() + { + $server = new Server(); + $memory = $this->getMock('OAuth2\Storage\Memory'); + $server->addStorage($memory, 'client_credentials'); + + $client = $server->getStorage('client'); + + $this->assertNotNull($client); + $this->assertEquals($client, $memory); + } + + public function testSettingClientStorageByDefaultDoesNotOverrideSetStorage() + { + $server = new Server(); + $pdo = $this->getMockBuilder('OAuth2\Storage\Pdo') + ->disableOriginalConstructor()->getMock(); + + $memory = $this->getMock('OAuth2\Storage\Memory'); + + $server->addStorage($pdo, 'client'); + $server->addStorage($memory, 'client_credentials'); + + $client = $server->getStorage('client'); + $client_credentials = $server->getStorage('client_credentials'); + + $this->assertEquals($client, $pdo); + $this->assertEquals($client_credentials, $memory); + } + + public function testAddingResponseType() + { + $storage = $this->getMock('OAuth2\Storage\Memory'); + $storage + ->expects($this->any()) + ->method('getClientDetails') + ->will($this->returnValue(array('client_id' => 'some_client'))); + $storage + ->expects($this->any()) + ->method('checkRestrictedGrantType') + ->will($this->returnValue(true)); + + // add with the "code" key explicitly set + $codeType = new AuthorizationCode($storage); + $server = new Server(); + $server->addStorage($storage); + $server->addResponseType($codeType); + $request = new Request(array( + 'response_type' => 'code', + 'client_id' => 'some_client', + 'redirect_uri' => 'http://example.com', + 'state' => 'xyx', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + // the response is successful + $this->assertEquals($response->getStatusCode(), 302); + $parts = parse_url($response->getHttpHeader('Location')); + parse_str($parts['query'], $query); + $this->assertTrue(isset($query['code'])); + $this->assertFalse(isset($query['error'])); + + // add with the "code" key not set + $codeType = new AuthorizationCode($storage); + $server = new Server(array($storage), array(), array(), array($codeType)); + $request = new Request(array( + 'response_type' => 'code', + 'client_id' => 'some_client', + 'redirect_uri' => 'http://example.com', + 'state' => 'xyx', + )); + $server->handleAuthorizeRequest($request, $response = new Response(), true); + + // the response is successful + $this->assertEquals($response->getStatusCode(), 302); + $parts = parse_url($response->getHttpHeader('Location')); + parse_str($parts['query'], $query); + $this->assertTrue(isset($query['code'])); + $this->assertFalse(isset($query['error'])); + } + + public function testCustomClientAssertionType() + { + $request = TestRequest::createPost(array( + 'grant_type' => 'authorization_code', + 'client_id' =>'Test Client ID', + 'code' => 'testcode', + )); + // verify the mock clientAssertionType was called as expected + $clientAssertionType = $this->getMock('OAuth2\ClientAssertionType\ClientAssertionTypeInterface', array('validateRequest', 'getClientId')); + $clientAssertionType + ->expects($this->once()) + ->method('validateRequest') + ->will($this->returnValue(true)); + $clientAssertionType + ->expects($this->once()) + ->method('getClientId') + ->will($this->returnValue('Test Client ID')); + + // create mock storage + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server(array($storage), array(), array(), array(), null, null, $clientAssertionType); + $server->handleTokenRequest($request, $response = new Response()); + } + + public function testHttpBasicConfig() + { + // create mock storage + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server(array($storage), array( + 'allow_credentials_in_request_body' => false, + 'allow_public_clients' => false + )); + $server->getTokenController(); + $httpBasic = $server->getClientAssertionType(); + + $reflection = new \ReflectionClass($httpBasic); + $prop = $reflection->getProperty('config'); + $prop->setAccessible(true); + + $config = $prop->getValue($httpBasic); // get the private "config" property + + $this->assertEquals($config['allow_credentials_in_request_body'], false); + $this->assertEquals($config['allow_public_clients'], false); + } + + public function testRefreshTokenConfig() + { + // create mock storage + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server1 = new Server(array($storage)); + $server2 = new Server(array($storage), array('always_issue_new_refresh_token' => true, 'unset_refresh_token_after_use' => false)); + + $server1->getTokenController(); + $refreshToken1 = $server1->getGrantType('refresh_token'); + + $server2->getTokenController(); + $refreshToken2 = $server2->getGrantType('refresh_token'); + + $reflection1 = new \ReflectionClass($refreshToken1); + $prop1 = $reflection1->getProperty('config'); + $prop1->setAccessible(true); + + $reflection2 = new \ReflectionClass($refreshToken2); + $prop2 = $reflection2->getProperty('config'); + $prop2->setAccessible(true); + + // get the private "config" property + $config1 = $prop1->getValue($refreshToken1); + $config2 = $prop2->getValue($refreshToken2); + + $this->assertEquals($config1['always_issue_new_refresh_token'], false); + $this->assertEquals($config2['always_issue_new_refresh_token'], true); + + $this->assertEquals($config1['unset_refresh_token_after_use'], true); + $this->assertEquals($config2['unset_refresh_token_after_use'], false); + } + + /** + * Test setting "always_issue_new_refresh_token" on a server level + * + * @see test/OAuth2/GrantType/RefreshTokenTest::testValidRefreshTokenWithNewRefreshTokenInResponse + **/ + public function testValidRefreshTokenWithNewRefreshTokenInResponse() + { + $storage = Bootstrap::getInstance()->getMemoryStorage(); + $server = new Server($storage, array('always_issue_new_refresh_token' => true)); + + $request = TestRequest::createPost(array( + 'grant_type' => 'refresh_token', // valid grant type + 'client_id' => 'Test Client ID', // valid client id + 'client_secret' => 'TestSecret', // valid client secret + 'refresh_token' => 'test-refreshtoken', // valid refresh token + )); + $token = $server->grantAccessToken($request, new Response()); + $this->assertTrue(isset($token['refresh_token']), 'refresh token should always refresh'); + + $refresh_token = $storage->getRefreshToken($token['refresh_token']); + $this->assertNotNull($refresh_token); + $this->assertEquals($refresh_token['refresh_token'], $token['refresh_token']); + $this->assertEquals($refresh_token['client_id'], $request->request('client_id')); + $this->assertTrue($token['refresh_token'] != 'test-refreshtoken', 'the refresh token returned is not the one used'); + $used_token = $storage->getRefreshToken('test-refreshtoken'); + $this->assertFalse($used_token, 'the refresh token used is no longer valid'); + } + + /** + * @expectedException InvalidArgumentException OAuth2\ResponseType\AuthorizationCodeInterface + **/ + public function testAddingUnknownResponseTypeThrowsException() + { + $server = new Server(); + $server->addResponseType($this->getMock('OAuth2\ResponseType\ResponseTypeInterface')); + } + + /** + * @expectedException LogicException OAuth2\Storage\PublicKeyInterface + **/ + public function testUsingJwtAccessTokensWithoutPublicKeyStorageThrowsException() + { + $server = new Server(array(), array('use_jwt_access_tokens' => true)); + $server->addGrantType($this->getMock('OAuth2\GrantType\GrantTypeInterface')); + $server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface')); + $server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface')); + + $server->getTokenController(); + } + + public function testUsingJustJwtAccessTokenStorageWithResourceControllerIsOkay() + { + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $server = new Server(array($pubkey), array('use_jwt_access_tokens' => true)); + + $this->assertNotNull($server->getResourceController()); + $this->assertInstanceOf('OAuth2\Storage\PublicKeyInterface', $server->getStorage('public_key')); + } + + /** + * @expectedException LogicException OAuth2\Storage\ClientInterface + **/ + public function testUsingJustJwtAccessTokenStorageWithAuthorizeControllerThrowsException() + { + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $server = new Server(array($pubkey), array('use_jwt_access_tokens' => true)); + $this->assertNotNull($server->getAuthorizeController()); + } + + /** + * @expectedException LogicException grant_types + **/ + public function testUsingJustJwtAccessTokenStorageWithTokenControllerThrowsException() + { + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $server = new Server(array($pubkey), array('use_jwt_access_tokens' => true)); + $server->getTokenController(); + } + + public function testUsingJwtAccessTokenAndClientStorageWithAuthorizeControllerIsOkay() + { + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $client = $this->getMock('OAuth2\Storage\ClientInterface'); + $server = new Server(array($pubkey, $client), array('use_jwt_access_tokens' => true, 'allow_implicit' => true)); + $this->assertNotNull($server->getAuthorizeController()); + + $this->assertInstanceOf('OAuth2\ResponseType\JwtAccessToken', $server->getResponseType('token')); + } + + /** + * @expectedException LogicException UserClaims + **/ + public function testUsingOpenIDConnectWithoutUserClaimsThrowsException() + { + $client = $this->getMock('OAuth2\Storage\ClientInterface'); + $server = new Server($client, array('use_openid_connect' => true)); + + $server->getAuthorizeController(); + } + + /** + * @expectedException LogicException PublicKeyInterface + **/ + public function testUsingOpenIDConnectWithoutPublicKeyThrowsException() + { + $client = $this->getMock('OAuth2\Storage\ClientInterface'); + $userclaims = $this->getMock('OAuth2\OPenID\Storage\UserClaimsInterface'); + $server = new Server(array($client, $userclaims), array('use_openid_connect' => true)); + + $server->getAuthorizeController(); + } + + /** + * @expectedException LogicException issuer + **/ + public function testUsingOpenIDConnectWithoutIssuerThrowsException() + { + $client = $this->getMock('OAuth2\Storage\ClientInterface'); + $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $server = new Server(array($client, $userclaims, $pubkey), array('use_openid_connect' => true)); + + $server->getAuthorizeController(); + } + + public function testUsingOpenIDConnectWithIssuerPublicKeyAndUserClaimsIsOkay() + { + $client = $this->getMock('OAuth2\Storage\ClientInterface'); + $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $server = new Server(array($client, $userclaims, $pubkey), array( + 'use_openid_connect' => true, + 'issuer' => 'someguy', + )); + + $server->getAuthorizeController(); + + $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token')); + $this->assertNull($server->getResponseType('id_token token')); + } + + /** + * @expectedException LogicException OAuth2\ResponseType\AccessTokenInterface + **/ + public function testUsingOpenIDConnectWithAllowImplicitWithoutTokenStorageThrowsException() + { + $client = $this->getMock('OAuth2\Storage\ClientInterface'); + $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $server = new Server(array($client, $userclaims, $pubkey), array( + 'use_openid_connect' => true, + 'issuer' => 'someguy', + 'allow_implicit' => true, + )); + + $server->getAuthorizeController(); + } + + public function testUsingOpenIDConnectWithAllowImplicitAndUseJwtAccessTokensIsOkay() + { + $client = $this->getMock('OAuth2\Storage\ClientInterface'); + $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $server = new Server(array($client, $userclaims, $pubkey), array( + 'use_openid_connect' => true, + 'issuer' => 'someguy', + 'allow_implicit' => true, + 'use_jwt_access_tokens' => true, + )); + + $server->getAuthorizeController(); + + $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token')); + $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenTokenInterface', $server->getResponseType('id_token token')); + } + + public function testUsingOpenIDConnectWithAllowImplicitAndAccessTokenStorageIsOkay() + { + $client = $this->getMock('OAuth2\Storage\ClientInterface'); + $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $token = $this->getMock('OAuth2\Storage\AccessTokenInterface'); + $server = new Server(array($client, $userclaims, $pubkey, $token), array( + 'use_openid_connect' => true, + 'issuer' => 'someguy', + 'allow_implicit' => true, + )); + + $server->getAuthorizeController(); + + $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token')); + $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenTokenInterface', $server->getResponseType('id_token token')); + } + + public function testUsingOpenIDConnectWithAllowImplicitAndAccessTokenResponseTypeIsOkay() + { + $client = $this->getMock('OAuth2\Storage\ClientInterface'); + $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + // $token = $this->getMock('OAuth2\Storage\AccessTokenInterface'); + $server = new Server(array($client, $userclaims, $pubkey), array( + 'use_openid_connect' => true, + 'issuer' => 'someguy', + 'allow_implicit' => true, + )); + + $token = $this->getMock('OAuth2\ResponseType\AccessTokenInterface'); + $server->addResponseType($token, 'token'); + + $server->getAuthorizeController(); + + $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token')); + $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenTokenInterface', $server->getResponseType('id_token token')); + } + + /** + * @expectedException LogicException OAuth2\OpenID\Storage\AuthorizationCodeInterface + **/ + public function testUsingOpenIDConnectWithAuthorizationCodeStorageThrowsException() + { + $client = $this->getMock('OAuth2\Storage\ClientCredentialsInterface'); + $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $token = $this->getMock('OAuth2\Storage\AccessTokenInterface'); + $authcode = $this->getMock('OAuth2\Storage\AuthorizationCodeInterface'); + + $server = new Server(array($client, $userclaims, $pubkey, $token, $authcode), array( + 'use_openid_connect' => true, + 'issuer' => 'someguy' + )); + + $server->getTokenController(); + + $this->assertInstanceOf('OAuth2\OpenID\GrantType\AuthorizationCode', $server->getGrantType('authorization_code')); + } + + public function testUsingOpenIDConnectWithOpenIDAuthorizationCodeStorageCreatesOpenIDAuthorizationCodeGrantType() + { + $client = $this->getMock('OAuth2\Storage\ClientCredentialsInterface'); + $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $token = $this->getMock('OAuth2\Storage\AccessTokenInterface'); + $authcode = $this->getMock('OAuth2\OpenID\Storage\AuthorizationCodeInterface'); + + $server = new Server(array($client, $userclaims, $pubkey, $token, $authcode), array( + 'use_openid_connect' => true, + 'issuer' => 'someguy' + )); + + $server->getTokenController(); + + $this->assertInstanceOf('OAuth2\OpenID\GrantType\AuthorizationCode', $server->getGrantType('authorization_code')); + } + + public function testMultipleValuedResponseTypeOrderDoesntMatter() + { + $responseType = $this->getMock('OAuth2\OpenID\ResponseType\IdTokenTokenInterface'); + $server = new Server(array(), array(), array(), array( + 'token id_token' => $responseType, + )); + + $this->assertEquals($responseType, $server->getResponseType('id_token token')); + } + + public function testAddGrantTypeWithoutKey() + { + $server = new Server(); + $server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($this->getMock('OAuth2\Storage\AuthorizationCodeInterface'))); + + $grantTypes = $server->getGrantTypes(); + $this->assertEquals('authorization_code', key($grantTypes)); + } + + public function testAddGrantTypeWithKey() + { + $server = new Server(); + $server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($this->getMock('OAuth2\Storage\AuthorizationCodeInterface')), 'ac'); + + $grantTypes = $server->getGrantTypes(); + $this->assertEquals('ac', key($grantTypes)); + } + + public function testAddGrantTypeWithKeyNotString() + { + $server = new Server(); + $server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($this->getMock('OAuth2\Storage\AuthorizationCodeInterface')), 42); + + $grantTypes = $server->getGrantTypes(); + $this->assertEquals('authorization_code', key($grantTypes)); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/AccessTokenTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/AccessTokenTest.php new file mode 100644 index 000000000..b34e0bfc0 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/AccessTokenTest.php @@ -0,0 +1,102 @@ +<?php + +namespace OAuth2\Storage; + +class AccessTokenTest extends BaseTest +{ + /** @dataProvider provideStorage */ + public function testSetAccessToken(AccessTokenInterface $storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // assert token we are about to add does not exist + $token = $storage->getAccessToken('newtoken'); + $this->assertFalse($token); + + // add new token + $expires = time() + 20; + $success = $storage->setAccessToken('newtoken', 'client ID', 'SOMEUSERID', $expires); + $this->assertTrue($success); + + $token = $storage->getAccessToken('newtoken'); + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('client_id', $token); + $this->assertArrayHasKey('user_id', $token); + $this->assertArrayHasKey('expires', $token); + $this->assertEquals($token['access_token'], 'newtoken'); + $this->assertEquals($token['client_id'], 'client ID'); + $this->assertEquals($token['user_id'], 'SOMEUSERID'); + $this->assertEquals($token['expires'], $expires); + + // change existing token + $expires = time() + 42; + $success = $storage->setAccessToken('newtoken', 'client ID2', 'SOMEOTHERID', $expires); + $this->assertTrue($success); + + $token = $storage->getAccessToken('newtoken'); + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + $this->assertArrayHasKey('client_id', $token); + $this->assertArrayHasKey('user_id', $token); + $this->assertArrayHasKey('expires', $token); + $this->assertEquals($token['access_token'], 'newtoken'); + $this->assertEquals($token['client_id'], 'client ID2'); + $this->assertEquals($token['user_id'], 'SOMEOTHERID'); + $this->assertEquals($token['expires'], $expires); + + // add token with scope having an empty string value + $expires = time() + 42; + $success = $storage->setAccessToken('newtoken', 'client ID', 'SOMEOTHERID', $expires, ''); + $this->assertTrue($success); + } + + /** @dataProvider provideStorage */ + public function testUnsetAccessToken(AccessTokenInterface $storage) + { + if ($storage instanceof NullStorage || !method_exists($storage, 'unsetAccessToken')) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // assert token we are about to unset does not exist + $token = $storage->getAccessToken('revokabletoken'); + $this->assertFalse($token); + + // add new token + $expires = time() + 20; + $success = $storage->setAccessToken('revokabletoken', 'client ID', 'SOMEUSERID', $expires); + $this->assertTrue($success); + + // assert unsetAccessToken returns true + $result = $storage->unsetAccessToken('revokabletoken'); + $this->assertTrue($result); + + // assert token we unset does not exist + $token = $storage->getAccessToken('revokabletoken'); + $this->assertFalse($token); + } + + /** @dataProvider provideStorage */ + public function testUnsetAccessTokenReturnsFalse(AccessTokenInterface $storage) + { + if ($storage instanceof NullStorage || !method_exists($storage, 'unsetAccessToken')) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // assert token we are about to unset does not exist + $token = $storage->getAccessToken('nonexistanttoken'); + $this->assertFalse($token); + + // assert unsetAccessToken returns false + $result = $storage->unsetAccessToken('nonexistanttoken'); + $this->assertFalse($result); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/AuthorizationCodeTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/AuthorizationCodeTest.php new file mode 100644 index 000000000..2d901b501 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/AuthorizationCodeTest.php @@ -0,0 +1,106 @@ +<?php + +namespace OAuth2\Storage; + +class AuthorizationCodeTest extends BaseTest +{ + /** @dataProvider provideStorage */ + public function testGetAuthorizationCode(AuthorizationCodeInterface $storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // nonexistant client_id + $details = $storage->getAuthorizationCode('faketoken'); + $this->assertFalse($details); + + // valid client_id + $details = $storage->getAuthorizationCode('testtoken'); + $this->assertNotNull($details); + } + + /** @dataProvider provideStorage */ + public function testSetAuthorizationCode(AuthorizationCodeInterface $storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // assert code we are about to add does not exist + $code = $storage->getAuthorizationCode('newcode'); + $this->assertFalse($code); + + // add new code + $expires = time() + 20; + $success = $storage->setAuthorizationCode('newcode', 'client ID', 'SOMEUSERID', 'http://example.com', $expires); + $this->assertTrue($success); + + $code = $storage->getAuthorizationCode('newcode'); + $this->assertNotNull($code); + $this->assertArrayHasKey('authorization_code', $code); + $this->assertArrayHasKey('client_id', $code); + $this->assertArrayHasKey('user_id', $code); + $this->assertArrayHasKey('redirect_uri', $code); + $this->assertArrayHasKey('expires', $code); + $this->assertEquals($code['authorization_code'], 'newcode'); + $this->assertEquals($code['client_id'], 'client ID'); + $this->assertEquals($code['user_id'], 'SOMEUSERID'); + $this->assertEquals($code['redirect_uri'], 'http://example.com'); + $this->assertEquals($code['expires'], $expires); + + // change existing code + $expires = time() + 42; + $success = $storage->setAuthorizationCode('newcode', 'client ID2', 'SOMEOTHERID', 'http://example.org', $expires); + $this->assertTrue($success); + + $code = $storage->getAuthorizationCode('newcode'); + $this->assertNotNull($code); + $this->assertArrayHasKey('authorization_code', $code); + $this->assertArrayHasKey('client_id', $code); + $this->assertArrayHasKey('user_id', $code); + $this->assertArrayHasKey('redirect_uri', $code); + $this->assertArrayHasKey('expires', $code); + $this->assertEquals($code['authorization_code'], 'newcode'); + $this->assertEquals($code['client_id'], 'client ID2'); + $this->assertEquals($code['user_id'], 'SOMEOTHERID'); + $this->assertEquals($code['redirect_uri'], 'http://example.org'); + $this->assertEquals($code['expires'], $expires); + + // add new code with scope having an empty string value + $expires = time() + 20; + $success = $storage->setAuthorizationCode('newcode', 'client ID', 'SOMEUSERID', 'http://example.com', $expires, ''); + $this->assertTrue($success); + } + + /** @dataProvider provideStorage */ + public function testExpireAccessToken(AccessTokenInterface $storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // create a valid code + $expires = time() + 20; + $success = $storage->setAuthorizationCode('code-to-expire', 'client ID', 'SOMEUSERID', 'http://example.com', time() + 20); + $this->assertTrue($success); + + // verify the new code exists + $code = $storage->getAuthorizationCode('code-to-expire'); + $this->assertNotNull($code); + + $this->assertArrayHasKey('authorization_code', $code); + $this->assertEquals($code['authorization_code'], 'code-to-expire'); + + // now expire the code and ensure it's no longer available + $storage->expireAuthorizationCode('code-to-expire'); + $code = $storage->getAuthorizationCode('code-to-expire'); + $this->assertFalse($code); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ClientCredentialsTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ClientCredentialsTest.php new file mode 100644 index 000000000..15289af30 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ClientCredentialsTest.php @@ -0,0 +1,28 @@ +<?php + +namespace OAuth2\Storage; + +class ClientCredentialsTest extends BaseTest +{ + /** @dataProvider provideStorage */ + public function testCheckClientCredentials(ClientCredentialsInterface $storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // nonexistant client_id + $pass = $storage->checkClientCredentials('fakeclient', 'testpass'); + $this->assertFalse($pass); + + // invalid password + $pass = $storage->checkClientCredentials('oauth_test_client', 'invalidcredentials'); + $this->assertFalse($pass); + + // valid credentials + $pass = $storage->checkClientCredentials('oauth_test_client', 'testpass'); + $this->assertTrue($pass); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ClientTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ClientTest.php new file mode 100644 index 000000000..6a5cc0b49 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ClientTest.php @@ -0,0 +1,110 @@ +<?php + +namespace OAuth2\Storage; + +class ClientTest extends BaseTest +{ + /** @dataProvider provideStorage */ + public function testGetClientDetails(ClientInterface $storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // nonexistant client_id + $details = $storage->getClientDetails('fakeclient'); + $this->assertFalse($details); + + // valid client_id + $details = $storage->getClientDetails('oauth_test_client'); + $this->assertNotNull($details); + $this->assertArrayHasKey('client_id', $details); + $this->assertArrayHasKey('client_secret', $details); + $this->assertArrayHasKey('redirect_uri', $details); + } + + /** @dataProvider provideStorage */ + public function testCheckRestrictedGrantType(ClientInterface $storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // Check invalid + $pass = $storage->checkRestrictedGrantType('oauth_test_client', 'authorization_code'); + $this->assertFalse($pass); + + // Check valid + $pass = $storage->checkRestrictedGrantType('oauth_test_client', 'implicit'); + $this->assertTrue($pass); + } + + /** @dataProvider provideStorage */ + public function testGetAccessToken(ClientInterface $storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // nonexistant client_id + $details = $storage->getAccessToken('faketoken'); + $this->assertFalse($details); + + // valid client_id + $details = $storage->getAccessToken('testtoken'); + $this->assertNotNull($details); + } + + /** @dataProvider provideStorage */ + public function testIsPublicClient(ClientInterface $storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + $publicClientId = 'public-client-'.rand(); + $confidentialClientId = 'confidential-client-'.rand(); + + // create a new client + $success1 = $storage->setClientDetails($publicClientId, ''); + $success2 = $storage->setClientDetails($confidentialClientId, 'some-secret'); + $this->assertTrue($success1); + $this->assertTrue($success2); + + // assert isPublicClient for both + $this->assertTrue($storage->isPublicClient($publicClientId)); + $this->assertFalse($storage->isPublicClient($confidentialClientId)); + } + + /** @dataProvider provideStorage */ + public function testSaveClient(ClientInterface $storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + $clientId = 'some-client-'.rand(); + + // create a new client + $success = $storage->setClientDetails($clientId, 'somesecret', 'http://test.com', 'client_credentials', 'clientscope1', 'brent@brentertainment.com'); + $this->assertTrue($success); + + // valid client_id + $details = $storage->getClientDetails($clientId); + $this->assertEquals($details['client_secret'], 'somesecret'); + $this->assertEquals($details['redirect_uri'], 'http://test.com'); + $this->assertEquals($details['grant_types'], 'client_credentials'); + $this->assertEquals($details['scope'], 'clientscope1'); + $this->assertEquals($details['user_id'], 'brent@brentertainment.com'); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/DynamoDBTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/DynamoDBTest.php new file mode 100644 index 000000000..2147f0914 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/DynamoDBTest.php @@ -0,0 +1,40 @@ +<?php + +namespace OAuth2\Storage; + +class DynamoDBTest extends BaseTest +{ + public function testGetDefaultScope() + { + $client = $this->getMockBuilder('\Aws\DynamoDb\DynamoDbClient') + ->disableOriginalConstructor() + ->setMethods(array('query')) + ->getMock(); + + $return = $this->getMockBuilder('\Guzzle\Service\Resource\Model') + ->setMethods(array('count', 'toArray')) + ->getMock(); + + $data = array( + 'Items' => array(), + 'Count' => 0, + 'ScannedCount'=> 0 + ); + + $return->expects($this->once()) + ->method('count') + ->will($this->returnValue(count($data))); + + $return->expects($this->once()) + ->method('toArray') + ->will($this->returnValue($data)); + + // should return null default scope if none is set in database + $client->expects($this->once()) + ->method('query') + ->will($this->returnValue($return)); + + $storage = new DynamoDB($client); + $this->assertNull($storage->getDefaultScope()); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/JwtAccessTokenTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/JwtAccessTokenTest.php new file mode 100644 index 000000000..a6acbea1f --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/JwtAccessTokenTest.php @@ -0,0 +1,41 @@ +<?php + +namespace OAuth2\Storage; + +use OAuth2\Encryption\Jwt; + +class JwtAccessTokenTest extends BaseTest +{ + /** @dataProvider provideStorage */ + public function testSetAccessToken($storage) + { + if (!$storage instanceof PublicKey) { + // incompatible storage + return; + } + + $crypto = new jwtAccessToken($storage); + + $publicKeyStorage = Bootstrap::getInstance()->getMemoryStorage(); + $encryptionUtil = new Jwt(); + + $jwtAccessToken = array( + 'access_token' => rand(), + 'expires' => time() + 100, + 'scope' => 'foo', + ); + + $token = $encryptionUtil->encode($jwtAccessToken, $storage->getPrivateKey(), $storage->getEncryptionAlgorithm()); + + $this->assertNotNull($token); + + $tokenData = $crypto->getAccessToken($token); + + $this->assertTrue(is_array($tokenData)); + + /* assert the decoded token is the same */ + $this->assertEquals($tokenData['access_token'], $jwtAccessToken['access_token']); + $this->assertEquals($tokenData['expires'], $jwtAccessToken['expires']); + $this->assertEquals($tokenData['scope'], $jwtAccessToken['scope']); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/JwtBearerTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/JwtBearerTest.php new file mode 100644 index 000000000..d0ab9b899 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/JwtBearerTest.php @@ -0,0 +1,25 @@ +<?php + +namespace OAuth2\Storage; + +class JwtBearerTest extends BaseTest +{ + /** @dataProvider provideStorage */ + public function testGetClientKey(JwtBearerInterface $storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // nonexistant client_id + $key = $storage->getClientKey('this-is-not-real', 'nor-is-this'); + $this->assertFalse($key); + + // valid client_id and subject + $key = $storage->getClientKey('oauth_test_client', 'test_subject'); + $this->assertNotNull($key); + $this->assertEquals($key, Bootstrap::getInstance()->getTestPublicKey()); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/PdoTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/PdoTest.php new file mode 100644 index 000000000..57eb39072 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/PdoTest.php @@ -0,0 +1,39 @@ +<?php + +namespace OAuth2\Storage; + +class PdoTest extends BaseTest +{ + public function testCreatePdoStorageUsingPdoClass() + { + $pdo = new \PDO(sprintf('sqlite://%s', Bootstrap::getInstance()->getSqliteDir())); + $storage = new Pdo($pdo); + + $this->assertNotNull($storage->getClientDetails('oauth_test_client')); + } + + public function testCreatePdoStorageUsingDSN() + { + $dsn = sprintf('sqlite://%s', Bootstrap::getInstance()->getSqliteDir()); + $storage = new Pdo($dsn); + + $this->assertNotNull($storage->getClientDetails('oauth_test_client')); + } + + public function testCreatePdoStorageUsingConfig() + { + $config = array('dsn' => sprintf('sqlite://%s', Bootstrap::getInstance()->getSqliteDir())); + $storage = new Pdo($config); + + $this->assertNotNull($storage->getClientDetails('oauth_test_client')); + } + + /** + * @expectedException InvalidArgumentException dsn + */ + public function testCreatePdoStorageWithoutDSNThrowsException() + { + $config = array('username' => 'brent', 'password' => 'brentisaballer'); + $storage = new Pdo($config); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/PublicKeyTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/PublicKeyTest.php new file mode 100644 index 000000000..f85195870 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/PublicKeyTest.php @@ -0,0 +1,29 @@ +<?php + +namespace OAuth2\Storage; + +class PublicKeyTest extends BaseTest +{ + /** @dataProvider provideStorage */ + public function testSetAccessToken($storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + if (!$storage instanceof PublicKeyInterface) { + // incompatible storage + return; + } + + $configDir = Bootstrap::getInstance()->getConfigDir(); + $globalPublicKey = file_get_contents($configDir.'/keys/id_rsa.pub'); + $globalPrivateKey = file_get_contents($configDir.'/keys/id_rsa'); + + /* assert values from storage */ + $this->assertEquals($storage->getPublicKey(), $globalPublicKey); + $this->assertEquals($storage->getPrivateKey(), $globalPrivateKey); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/RefreshTokenTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/RefreshTokenTest.php new file mode 100644 index 000000000..314c93195 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/RefreshTokenTest.php @@ -0,0 +1,41 @@ +<?php + +namespace OAuth2\Storage; + +class RefreshTokenTest extends BaseTest +{ + /** @dataProvider provideStorage */ + public function testSetRefreshToken(RefreshTokenInterface $storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // assert token we are about to add does not exist + $token = $storage->getRefreshToken('refreshtoken'); + $this->assertFalse($token); + + // add new token + $expires = time() + 20; + $success = $storage->setRefreshToken('refreshtoken', 'client ID', 'SOMEUSERID', $expires); + $this->assertTrue($success); + + $token = $storage->getRefreshToken('refreshtoken'); + $this->assertNotNull($token); + $this->assertArrayHasKey('refresh_token', $token); + $this->assertArrayHasKey('client_id', $token); + $this->assertArrayHasKey('user_id', $token); + $this->assertArrayHasKey('expires', $token); + $this->assertEquals($token['refresh_token'], 'refreshtoken'); + $this->assertEquals($token['client_id'], 'client ID'); + $this->assertEquals($token['user_id'], 'SOMEUSERID'); + $this->assertEquals($token['expires'], $expires); + + // add token with scope having an empty string value + $expires = time() + 20; + $success = $storage->setRefreshToken('refreshtoken2', 'client ID', 'SOMEUSERID', $expires, ''); + $this->assertTrue($success); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ScopeTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ScopeTest.php new file mode 100644 index 000000000..fd1edeb93 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/ScopeTest.php @@ -0,0 +1,53 @@ +<?php + +namespace OAuth2\Storage; + +use OAuth2\Scope; + +class ScopeTest extends BaseTest +{ + /** @dataProvider provideStorage */ + public function testScopeExists($storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + if (!$storage instanceof ScopeInterface) { + // incompatible storage + return; + } + + //Test getting scopes + $scopeUtil = new Scope($storage); + $this->assertTrue($scopeUtil->scopeExists('supportedscope1')); + $this->assertTrue($scopeUtil->scopeExists('supportedscope1 supportedscope2 supportedscope3')); + $this->assertFalse($scopeUtil->scopeExists('fakescope')); + $this->assertFalse($scopeUtil->scopeExists('supportedscope1 supportedscope2 supportedscope3 fakescope')); + } + + /** @dataProvider provideStorage */ + public function testGetDefaultScope($storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + if (!$storage instanceof ScopeInterface) { + // incompatible storage + return; + } + + // test getting default scope + $scopeUtil = new Scope($storage); + $expected = explode(' ', $scopeUtil->getDefaultScope()); + $actual = explode(' ', 'defaultscope1 defaultscope2'); + sort($expected); + sort($actual); + $this->assertEquals($expected, $actual); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/UserCredentialsTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/UserCredentialsTest.php new file mode 100644 index 000000000..65655a6b2 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/Storage/UserCredentialsTest.php @@ -0,0 +1,40 @@ +<?php + +namespace OAuth2\Storage; + +class UserCredentialsTest extends BaseTest +{ + /** @dataProvider provideStorage */ + public function testCheckUserCredentials(UserCredentialsInterface $storage) + { + if ($storage instanceof NullStorage) { + $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); + + return; + } + + // create a new user for testing + $success = $storage->setUser('testusername', 'testpass', 'Test', 'User'); + $this->assertTrue($success); + + // correct credentials + $this->assertTrue($storage->checkUserCredentials('testusername', 'testpass')); + // invalid password + $this->assertFalse($storage->checkUserCredentials('testusername', 'fakepass')); + // invalid username + $this->assertFalse($storage->checkUserCredentials('fakeusername', 'testpass')); + + // invalid username + $this->assertFalse($storage->getUserDetails('fakeusername')); + + // ensure all properties are set + $user = $storage->getUserDetails('testusername'); + $this->assertTrue($user !== false); + $this->assertArrayHasKey('user_id', $user); + $this->assertArrayHasKey('first_name', $user); + $this->assertArrayHasKey('last_name', $user); + $this->assertEquals($user['user_id'], 'testusername'); + $this->assertEquals($user['first_name'], 'Test'); + $this->assertEquals($user['last_name'], 'User'); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/OAuth2/TokenType/BearerTest.php b/vendor/bshaffer/oauth2-server-php/test/OAuth2/TokenType/BearerTest.php new file mode 100644 index 000000000..a2e000e22 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/OAuth2/TokenType/BearerTest.php @@ -0,0 +1,58 @@ +<?php + +namespace OAuth2\TokenType; + +use OAuth2\Request\TestRequest; +use OAuth2\Response; + +class BearerTest extends \PHPUnit_Framework_TestCase +{ + public function testValidContentTypeWithCharset() + { + $bearer = new Bearer(); + $request = TestRequest::createPost(array( + 'access_token' => 'ThisIsMyAccessToken' + )); + $request->server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=UTF-8'; + + $param = $bearer->getAccessTokenParameter($request, $response = new Response()); + $this->assertEquals($param, 'ThisIsMyAccessToken'); + } + + public function testInvalidContentType() + { + $bearer = new Bearer(); + $request = TestRequest::createPost(array( + 'access_token' => 'ThisIsMyAccessToken' + )); + $request->server['CONTENT_TYPE'] = 'application/json; charset=UTF-8'; + + $param = $bearer->getAccessTokenParameter($request, $response = new Response()); + $this->assertNull($param); + $this->assertEquals($response->getStatusCode(), 400); + $this->assertEquals($response->getParameter('error'), 'invalid_request'); + $this->assertEquals($response->getParameter('error_description'), 'The content type for POST requests must be "application/x-www-form-urlencoded"'); + } + + public function testValidRequestUsingAuthorizationHeader() + { + $bearer = new Bearer(); + $request = new TestRequest(); + $request->headers['AUTHORIZATION'] = 'Bearer MyToken'; + $request->server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=UTF-8'; + + $param = $bearer->getAccessTokenParameter($request, $response = new Response()); + $this->assertEquals('MyToken', $param); + } + + public function testValidRequestUsingAuthorizationHeaderCaseInsensitive() + { + $bearer = new Bearer(); + $request = new TestRequest(); + $request->server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=UTF-8'; + $request->headers['Authorization'] = 'Bearer MyToken'; + + $param = $bearer->getAccessTokenParameter($request, $response = new Response()); + $this->assertEquals('MyToken', $param); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/bootstrap.php b/vendor/bshaffer/oauth2-server-php/test/bootstrap.php new file mode 100644 index 000000000..0a4af0716 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/bootstrap.php @@ -0,0 +1,12 @@ +<?php + +require_once(dirname(__FILE__).'/../src/OAuth2/Autoloader.php'); +OAuth2\Autoloader::register(); + +// register test classes +OAuth2\Autoloader::register(dirname(__FILE__).'/lib'); + +// register vendors if possible +if (file_exists(__DIR__.'/../vendor/autoload.php')) { + require_once(__DIR__.'/../vendor/autoload.php'); +} diff --git a/vendor/bshaffer/oauth2-server-php/test/cleanup.php b/vendor/bshaffer/oauth2-server-php/test/cleanup.php new file mode 100644 index 000000000..8663a901b --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/cleanup.php @@ -0,0 +1,15 @@ +<?php + +require_once(dirname(__FILE__).'/../src/OAuth2/Autoloader.php'); +OAuth2\Autoloader::register(); + +// register test classes +OAuth2\Autoloader::register(dirname(__FILE__).'/lib'); + +// register vendors if possible +if (file_exists(__DIR__.'/../vendor/autoload.php')) { + require_once(__DIR__.'/../vendor/autoload.php'); +} + +// remove the dynamoDB database that was created for this build +OAuth2\Storage\Bootstrap::getInstance()->cleanupTravisDynamoDb(); diff --git a/vendor/bshaffer/oauth2-server-php/test/config/keys/id_rsa b/vendor/bshaffer/oauth2-server-php/test/config/keys/id_rsa new file mode 100644 index 000000000..e8b9eff2d --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/config/keys/id_rsa @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQC8fpi06NfVYHAOAnxNMVnTXr/ptsLsNjP+uAt2eO0cc5J9H5XV +8lFVujOrRu/JWi1TDmAvOaf/6A3BphIA1Pwp0AAqlZdwizIum8j0KzpsGYH5qReN +QDwF3oUSKMsQCCGCDHrDYifG/pRi9bN1ZVjEXPr35HJuBe+FQpZTs8DewwIDAQAB +AoGARfNxNknmtx/n1bskZ/01iZRzAge6BLEE0LV6Q4gS7mkRZu/Oyiv39Sl5vUlA ++WdGxLjkBwKNjxGN8Vxw9/ASd8rSsqeAUYIwAeifXrHhj5DBPQT/pDPkeFnp9B1w +C6jo+3AbBQ4/b0ONSIEnCL2xGGglSIAxO17T1ViXp7lzXPECQQDe63nkRdWM0OCb +oaHQPT3E26224maIstrGFUdt9yw3yJf4bOF7TtiPLlLuHsTTge3z+fG6ntC0xG56 +1cl37C3ZAkEA2HdVcRGugNp/qmVz4LJTpD+WZKi73PLAO47wDOrYh9Pn2I6fcEH0 +CPnggt1ko4ujvGzFTvRH64HXa6aPCv1j+wJBAMQMah3VQPNf/DlDVFEUmw9XeBZg +VHaifX851aEjgXLp6qVj9IYCmLiLsAmVa9rr6P7p8asD418nZlaHUHE0eDkCQQCr +uxis6GMx1Ka971jcJX2X696LoxXPd0KsvXySMupv79yagKPa8mgBiwPjrnK+EPVo +cj6iochA/bSCshP/mwFrAkBHEKPi6V6gb94JinCT7x3weahbdp6bJ6/nzBH/p9VA +HoT1JtwNFhGv9BCjmDydshQHfSWpY9NxlccBKL7ITm8R +-----END RSA PRIVATE KEY-----
\ No newline at end of file diff --git a/vendor/bshaffer/oauth2-server-php/test/config/keys/id_rsa.pub b/vendor/bshaffer/oauth2-server-php/test/config/keys/id_rsa.pub new file mode 100644 index 000000000..1ac15f5eb --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/config/keys/id_rsa.pub @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICiDCCAfGgAwIBAgIBADANBgkqhkiG9w0BAQQFADA9MQswCQYDVQQGEwJVUzEL +MAkGA1UECBMCVVQxITAfBgNVBAoTGFZpZ25ldHRlIENvcnBvcmF0aW9uIFNCWDAe +Fw0xMTEwMTUwMzE4MjdaFw0zMTEwMTAwMzE4MjdaMD0xCzAJBgNVBAYTAlVTMQsw +CQYDVQQIEwJVVDEhMB8GA1UEChMYVmlnbmV0dGUgQ29ycG9yYXRpb24gU0JYMIGf +MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8fpi06NfVYHAOAnxNMVnTXr/ptsLs +NjP+uAt2eO0cc5J9H5XV8lFVujOrRu/JWi1TDmAvOaf/6A3BphIA1Pwp0AAqlZdw +izIum8j0KzpsGYH5qReNQDwF3oUSKMsQCCGCDHrDYifG/pRi9bN1ZVjEXPr35HJu +Be+FQpZTs8DewwIDAQABo4GXMIGUMB0GA1UdDgQWBBRe8hrEXm+Yim4YlD5Nx+1K +vCYs9DBlBgNVHSMEXjBcgBRe8hrEXm+Yim4YlD5Nx+1KvCYs9KFBpD8wPTELMAkG +A1UEBhMCVVMxCzAJBgNVBAgTAlVUMSEwHwYDVQQKExhWaWduZXR0ZSBDb3Jwb3Jh +dGlvbiBTQliCAQAwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQQFAAOBgQBjhyRD +lM7vnLn6drgQVftW5V9nDFAyPAuiGvMIKFSbiAf1PxXCRn5sfJquwWKsJUi4ZGNl +aViXdFmN6/F13PSM+yg63tpKy0fYqMbTM+Oe5WuSHkSW1VuYNHV+24adgNk/FRDL +FRrlM1f6s9VTLWvwGItjfrof0Ba8Uq7ZDSb9Xg== +-----END CERTIFICATE-----
\ No newline at end of file diff --git a/vendor/bshaffer/oauth2-server-php/test/config/storage.json b/vendor/bshaffer/oauth2-server-php/test/config/storage.json new file mode 100644 index 000000000..52d3f2399 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/config/storage.json @@ -0,0 +1,188 @@ +{ + "authorization_codes": { + "testcode": { + "client_id": "Test Client ID", + "user_id": "", + "redirect_uri": "", + "expires": "9999999999", + "id_token": "IDTOKEN" + }, + "testcode-with-scope": { + "client_id": "Test Client ID", + "user_id": "", + "redirect_uri": "", + "expires": "9999999999", + "scope": "scope1 scope2" + }, + "testcode-expired": { + "client_id": "Test Client ID", + "user_id": "", + "redirect_uri": "", + "expires": "1356998400" + }, + "testcode-empty-secret": { + "client_id": "Test Client ID Empty Secret", + "user_id": "", + "redirect_uri": "", + "expires": "9999999999" + }, + "testcode-openid": { + "client_id": "Test Client ID", + "user_id": "", + "redirect_uri": "", + "expires": "9999999999", + "id_token": "test_id_token" + }, + "testcode-redirect-uri": { + "client_id": "Test Client ID", + "user_id": "", + "redirect_uri": "http://brentertainment.com/voil%C3%A0", + "expires": "9999999999", + "id_token": "IDTOKEN" + } + }, + "client_credentials" : { + "Test Client ID": { + "client_secret": "TestSecret" + }, + "Test Client ID with Redirect Uri": { + "client_secret": "TestSecret2", + "redirect_uri": "http://brentertainment.com" + }, + "Test Client ID with Buggy Redirect Uri": { + "client_secret": "TestSecret2", + "redirect_uri": " http://brentertainment.com" + }, + "Test Client ID with Multiple Redirect Uris": { + "client_secret": "TestSecret3", + "redirect_uri": "http://brentertainment.com http://morehazards.com" + }, + "Test Client ID with Redirect Uri Parts": { + "client_secret": "TestSecret4", + "redirect_uri": "http://user:pass@brentertainment.com:2222/authorize/cb?auth_type=oauth&test=true" + }, + "Test Some Other Client": { + "client_secret": "TestSecret3" + }, + "Test Client ID Empty Secret": { + "client_secret": "" + }, + "Test Client ID For Password Grant": { + "grant_types": "password", + "client_secret": "" + }, + "Client ID With User ID": { + "client_secret": "TestSecret", + "user_id": "brent@brentertainment.com" + }, + "oauth_test_client": { + "client_secret": "testpass", + "grant_types": "implicit password" + } + }, + "user_credentials" : { + "test-username": { + "password": "testpass" + }, + "testusername": { + "password": "testpass" + }, + "testuser": { + "password": "password", + "email": "testuser@test.com", + "email_verified": true + }, + "johndoe": { + "password": "password" + } + }, + "refresh_tokens" : { + "test-refreshtoken": { + "refresh_token": "test-refreshtoken", + "client_id": "Test Client ID", + "user_id": "test-username", + "expires": 0, + "scope": null + }, + "test-refreshtoken-with-scope": { + "refresh_token": "test-refreshtoken", + "client_id": "Test Client ID", + "user_id": "test-username", + "expires": 0, + "scope": "scope1 scope2" + } + }, + "access_tokens" : { + "accesstoken-expired": { + "access_token": "accesstoken-expired", + "client_id": "Test Client ID", + "expires": 1234567, + "scope": null + }, + "accesstoken-scope": { + "access_token": "accesstoken-scope", + "client_id": "Test Client ID", + "expires": 99999999900, + "scope": "testscope" + }, + "accesstoken-openid-connect": { + "access_token": "accesstoken-openid-connect", + "client_id": "Test Client ID", + "user_id": "testuser", + "expires": 99999999900, + "scope": "openid email" + }, + "accesstoken-malformed": { + "access_token": "accesstoken-mallformed", + "expires": 99999999900, + "scope": "testscope" + } + }, + "jwt": { + "Test Client ID": { + "key": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5/SxVlE8gnpFqCxgl2wjhzY7u\ncEi00s0kUg3xp7lVEvgLgYcAnHiWp+gtSjOFfH2zsvpiWm6Lz5f743j/FEzHIO1o\nwR0p4d9pOaJK07d01+RzoQLOIQAgXrr4T1CCWUesncwwPBVCyy2Mw3Nmhmr9MrF8\nUlvdRKBxriRnlP3qJQIDAQAB\n-----END PUBLIC KEY-----", + "subject": "testuser@ourdomain.com" + }, + "Test Client ID PHP-5.2": { + "key": "mysecretkey", + "subject": "testuser@ourdomain.com" + }, + "Missing Key Client": { + "key": null, + "subject": "testuser@ourdomain.com" + }, + "Missing Key Client PHP-5.2": { + "key": null, + "subject": "testuser@ourdomain.com" + }, + "oauth_test_client": { + "key": "-----BEGIN CERTIFICATE-----\nMIICiDCCAfGgAwIBAgIBADANBgkqhkiG9w0BAQQFADA9MQswCQYDVQQGEwJVUzEL\nMAkGA1UECBMCVVQxITAfBgNVBAoTGFZpZ25ldHRlIENvcnBvcmF0aW9uIFNCWDAe\nFw0xMTEwMTUwMzE4MjdaFw0zMTEwMTAwMzE4MjdaMD0xCzAJBgNVBAYTAlVTMQsw\nCQYDVQQIEwJVVDEhMB8GA1UEChMYVmlnbmV0dGUgQ29ycG9yYXRpb24gU0JYMIGf\nMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8fpi06NfVYHAOAnxNMVnTXr/ptsLs\nNjP+uAt2eO0cc5J9H5XV8lFVujOrRu/JWi1TDmAvOaf/6A3BphIA1Pwp0AAqlZdw\nizIum8j0KzpsGYH5qReNQDwF3oUSKMsQCCGCDHrDYifG/pRi9bN1ZVjEXPr35HJu\nBe+FQpZTs8DewwIDAQABo4GXMIGUMB0GA1UdDgQWBBRe8hrEXm+Yim4YlD5Nx+1K\nvCYs9DBlBgNVHSMEXjBcgBRe8hrEXm+Yim4YlD5Nx+1KvCYs9KFBpD8wPTELMAkG\nA1UEBhMCVVMxCzAJBgNVBAgTAlVUMSEwHwYDVQQKExhWaWduZXR0ZSBDb3Jwb3Jh\ndGlvbiBTQliCAQAwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQQFAAOBgQBjhyRD\nlM7vnLn6drgQVftW5V9nDFAyPAuiGvMIKFSbiAf1PxXCRn5sfJquwWKsJUi4ZGNl\naViXdFmN6/F13PSM+yg63tpKy0fYqMbTM+Oe5WuSHkSW1VuYNHV+24adgNk/FRDL\nFRrlM1f6s9VTLWvwGItjfrof0Ba8Uq7ZDSb9Xg==\n-----END CERTIFICATE-----", + "subject": "test_subject" + } + }, + "jti": [ + { + "issuer": "Test Client ID", + "subject": "testuser@ourdomain.com", + "audience": "http://myapp.com/oauth/auth", + "expires": 99999999900, + "jti": "used_jti" + } + ], + "supported_scopes" : [ + "scope1", + "scope2", + "scope3", + "clientscope1", + "clientscope2", + "clientscope3", + "supportedscope1", + "supportedscope2", + "supportedscope3", + "supportedscope4" + ], + "keys": { + "public_key": "-----BEGIN CERTIFICATE-----\nMIICiDCCAfGgAwIBAgIBADANBgkqhkiG9w0BAQQFADA9MQswCQYDVQQGEwJVUzEL\nMAkGA1UECBMCVVQxITAfBgNVBAoTGFZpZ25ldHRlIENvcnBvcmF0aW9uIFNCWDAe\nFw0xMTEwMTUwMzE4MjdaFw0zMTEwMTAwMzE4MjdaMD0xCzAJBgNVBAYTAlVTMQsw\nCQYDVQQIEwJVVDEhMB8GA1UEChMYVmlnbmV0dGUgQ29ycG9yYXRpb24gU0JYMIGf\nMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8fpi06NfVYHAOAnxNMVnTXr/ptsLs\nNjP+uAt2eO0cc5J9H5XV8lFVujOrRu/JWi1TDmAvOaf/6A3BphIA1Pwp0AAqlZdw\nizIum8j0KzpsGYH5qReNQDwF3oUSKMsQCCGCDHrDYifG/pRi9bN1ZVjEXPr35HJu\nBe+FQpZTs8DewwIDAQABo4GXMIGUMB0GA1UdDgQWBBRe8hrEXm+Yim4YlD5Nx+1K\nvCYs9DBlBgNVHSMEXjBcgBRe8hrEXm+Yim4YlD5Nx+1KvCYs9KFBpD8wPTELMAkG\nA1UEBhMCVVMxCzAJBgNVBAgTAlVUMSEwHwYDVQQKExhWaWduZXR0ZSBDb3Jwb3Jh\ndGlvbiBTQliCAQAwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQQFAAOBgQBjhyRD\nlM7vnLn6drgQVftW5V9nDFAyPAuiGvMIKFSbiAf1PxXCRn5sfJquwWKsJUi4ZGNl\naViXdFmN6/F13PSM+yg63tpKy0fYqMbTM+Oe5WuSHkSW1VuYNHV+24adgNk/FRDL\nFRrlM1f6s9VTLWvwGItjfrof0Ba8Uq7ZDSb9Xg==\n-----END CERTIFICATE-----", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIICXQIBAAKBgQC8fpi06NfVYHAOAnxNMVnTXr/ptsLsNjP+uAt2eO0cc5J9H5XV\n8lFVujOrRu/JWi1TDmAvOaf/6A3BphIA1Pwp0AAqlZdwizIum8j0KzpsGYH5qReN\nQDwF3oUSKMsQCCGCDHrDYifG/pRi9bN1ZVjEXPr35HJuBe+FQpZTs8DewwIDAQAB\nAoGARfNxNknmtx/n1bskZ/01iZRzAge6BLEE0LV6Q4gS7mkRZu/Oyiv39Sl5vUlA\n+WdGxLjkBwKNjxGN8Vxw9/ASd8rSsqeAUYIwAeifXrHhj5DBPQT/pDPkeFnp9B1w\nC6jo+3AbBQ4/b0ONSIEnCL2xGGglSIAxO17T1ViXp7lzXPECQQDe63nkRdWM0OCb\noaHQPT3E26224maIstrGFUdt9yw3yJf4bOF7TtiPLlLuHsTTge3z+fG6ntC0xG56\n1cl37C3ZAkEA2HdVcRGugNp/qmVz4LJTpD+WZKi73PLAO47wDOrYh9Pn2I6fcEH0\nCPnggt1ko4ujvGzFTvRH64HXa6aPCv1j+wJBAMQMah3VQPNf/DlDVFEUmw9XeBZg\nVHaifX851aEjgXLp6qVj9IYCmLiLsAmVa9rr6P7p8asD418nZlaHUHE0eDkCQQCr\nuxis6GMx1Ka971jcJX2X696LoxXPd0KsvXySMupv79yagKPa8mgBiwPjrnK+EPVo\ncj6iochA/bSCshP/mwFrAkBHEKPi6V6gb94JinCT7x3weahbdp6bJ6/nzBH/p9VA\nHoT1JtwNFhGv9BCjmDydshQHfSWpY9NxlccBKL7ITm8R\n-----END RSA PRIVATE KEY-----" + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Request/TestRequest.php b/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Request/TestRequest.php new file mode 100644 index 000000000..7bbce28a4 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Request/TestRequest.php @@ -0,0 +1,61 @@ +<?php + +namespace OAuth2\Request; + +use OAuth2\Request; +use OAuth2\RequestInterface; + +/** +* +*/ +class TestRequest extends Request implements RequestInterface +{ + public $query, $request, $server, $headers; + + public function __construct() + { + $this->query = $_GET; + $this->request = $_POST; + $this->server = $_SERVER; + $this->headers = array(); + } + + public function query($name, $default = null) + { + return isset($this->query[$name]) ? $this->query[$name] : $default; + } + + public function request($name, $default = null) + { + return isset($this->request[$name]) ? $this->request[$name] : $default; + } + + public function server($name, $default = null) + { + return isset($this->server[$name]) ? $this->server[$name] : $default; + } + + public function getAllQueryParameters() + { + return $this->query; + } + + public function setQuery(array $query) + { + $this->query = $query; + } + + public function setPost(array $params) + { + $this->server['REQUEST_METHOD'] = 'POST'; + $this->request = $params; + } + + public static function createPost(array $params = array()) + { + $request = new self(); + $request->setPost($params); + + return $request; + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/BaseTest.php b/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/BaseTest.php new file mode 100755 index 000000000..f0b1274a2 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/BaseTest.php @@ -0,0 +1,36 @@ +<?php + +namespace OAuth2\Storage; + +abstract class BaseTest extends \PHPUnit_Framework_TestCase +{ + public function provideStorage() + { + $memory = Bootstrap::getInstance()->getMemoryStorage(); + $sqlite = Bootstrap::getInstance()->getSqlitePdo(); + $mysql = Bootstrap::getInstance()->getMysqlPdo(); + $postgres = Bootstrap::getInstance()->getPostgresPdo(); + $mongo = Bootstrap::getInstance()->getMongo(); + $mongoDb = Bootstrap::getInstance()->getMongoDB(); + $redis = Bootstrap::getInstance()->getRedisStorage(); + $cassandra = Bootstrap::getInstance()->getCassandraStorage(); + $dynamodb = Bootstrap::getInstance()->getDynamoDbStorage(); + $couchbase = Bootstrap::getInstance()->getCouchbase(); + + /* hack until we can fix "default_scope" dependencies in other tests */ + $memory->defaultScope = 'defaultscope1 defaultscope2'; + + return array( + array($memory), + array($sqlite), + array($mysql), + array($postgres), + array($mongo), + array($mongoDb), + array($redis), + array($cassandra), + array($dynamodb), + array($couchbase), + ); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/Bootstrap.php b/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/Bootstrap.php new file mode 100755 index 000000000..3d7bdd4e9 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/Bootstrap.php @@ -0,0 +1,967 @@ +<?php + +namespace OAuth2\Storage; + +class Bootstrap +{ + const DYNAMODB_PHP_VERSION = 'none'; + + protected static $instance; + private $mysql; + private $sqlite; + private $postgres; + private $mongo; + private $mongoDb; + private $redis; + private $cassandra; + private $configDir; + private $dynamodb; + private $couchbase; + + public function __construct() + { + $this->configDir = __DIR__.'/../../../config'; + } + + public static function getInstance() + { + if (!self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + public function getSqlitePdo() + { + if (!$this->sqlite) { + $this->removeSqliteDb(); + $pdo = new \PDO(sprintf('sqlite://%s', $this->getSqliteDir())); + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + $this->createSqliteDb($pdo); + + $this->sqlite = new Pdo($pdo); + } + + return $this->sqlite; + } + + public function getPostgresPdo() + { + if (!$this->postgres) { + if (in_array('pgsql', \PDO::getAvailableDrivers())) { + $this->removePostgresDb(); + $this->createPostgresDb(); + if ($pdo = $this->getPostgresDriver()) { + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + $this->populatePostgresDb($pdo); + $this->postgres = new Pdo($pdo); + } + } else { + $this->postgres = new NullStorage('Postgres', 'Missing postgres PDO extension.'); + } + } + + return $this->postgres; + } + + public function getPostgresDriver() + { + try { + $pdo = new \PDO('pgsql:host=localhost;dbname=oauth2_server_php', 'postgres'); + + return $pdo; + } catch (\PDOException $e) { + $this->postgres = new NullStorage('Postgres', $e->getMessage()); + } + } + + public function getMemoryStorage() + { + return new Memory(json_decode(file_get_contents($this->configDir. '/storage.json'), true)); + } + + public function getRedisStorage() + { + if (!$this->redis) { + if (class_exists('Predis\Client')) { + $redis = new \Predis\Client(); + if ($this->testRedisConnection($redis)) { + $redis->flushdb(); + $this->redis = new Redis($redis); + $this->createRedisDb($this->redis); + } else { + $this->redis = new NullStorage('Redis', 'Unable to connect to redis server on port 6379'); + } + } else { + $this->redis = new NullStorage('Redis', 'Missing redis library. Please run "composer.phar require predis/predis:dev-master"'); + } + } + + return $this->redis; + } + + private function testRedisConnection(\Predis\Client $redis) + { + try { + $redis->connect(); + } catch (\Predis\CommunicationException $exception) { + // we were unable to connect to the redis server + return false; + } + + return true; + } + + public function getMysqlPdo() + { + if (!$this->mysql) { + $pdo = null; + try { + $pdo = new \PDO('mysql:host=localhost;', 'root'); + } catch (\PDOException $e) { + $this->mysql = new NullStorage('MySQL', 'Unable to connect to MySQL on root@localhost'); + } + + if ($pdo) { + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + $this->removeMysqlDb($pdo); + $this->createMysqlDb($pdo); + + $this->mysql = new Pdo($pdo); + } + } + + return $this->mysql; + } + + public function getMongo() + { + if (!$this->mongo) { + if (class_exists('MongoClient')) { + $mongo = new \MongoClient('mongodb://localhost:27017', array('connect' => false)); + if ($this->testMongoConnection($mongo)) { + $db = $mongo->oauth2_server_php_legacy; + $this->removeMongo($db); + $this->createMongo($db); + + $this->mongo = new Mongo($db); + } else { + $this->mongo = new NullStorage('Mongo', 'Unable to connect to mongo server on "localhost:27017"'); + } + } else { + $this->mongo = new NullStorage('Mongo', 'Missing mongo php extension. Please install mongo.so'); + } + } + + return $this->mongo; + } + + public function getMongoDb() + { + if (!$this->mongoDb) { + if (class_exists('MongoDB\Client')) { + $mongoDb = new \MongoDB\Client('mongodb://localhost:27017'); + if ($this->testMongoDBConnection($mongoDb)) { + $db = $mongoDb->oauth2_server_php; + $this->removeMongoDb($db); + $this->createMongoDb($db); + + $this->mongoDb = new MongoDB($db); + } else { + $this->mongoDb = new NullStorage('MongoDB', 'Unable to connect to mongo server on "localhost:27017"'); + } + } else { + $this->mongoDb = new NullStorage('MongoDB', 'Missing MongoDB php extension. Please install mongodb.so'); + } + } + + return $this->mongoDb; + } + + private function testMongoConnection(\MongoClient $mongo) + { + try { + $mongo->connect(); + } catch (\MongoConnectionException $e) { + return false; + } + + return true; + } + + private function testMongoDBConnection(\MongoDB\Client $mongo) + { + return true; + } + + public function getCouchbase() + { + if (!$this->couchbase) { + if ($this->getEnvVar('SKIP_COUCHBASE_TESTS')) { + $this->couchbase = new NullStorage('Couchbase', 'Skipping Couchbase tests'); + } elseif (!class_exists('Couchbase')) { + $this->couchbase = new NullStorage('Couchbase', 'Missing Couchbase php extension. Please install couchbase.so'); + } else { + // round-about way to make sure couchbase is working + // this is required because it throws a "floating point exception" otherwise + $code = "new \Couchbase(array('localhost:8091'), '', '', 'auth', false);"; + $exec = sprintf('php -r "%s"', $code); + $ret = exec($exec, $test, $var); + if ($ret != 0) { + $couchbase = new \Couchbase(array('localhost:8091'), '', '', 'auth', false); + if ($this->testCouchbaseConnection($couchbase)) { + $this->clearCouchbase($couchbase); + $this->createCouchbaseDB($couchbase); + + $this->couchbase = new CouchbaseDB($couchbase); + } else { + $this->couchbase = new NullStorage('Couchbase', 'Unable to connect to Couchbase server on "localhost:8091"'); + } + } else { + $this->couchbase = new NullStorage('Couchbase', 'Error while trying to connect to Couchbase'); + } + } + } + + return $this->couchbase; + } + + private function testCouchbaseConnection(\Couchbase $couchbase) + { + try { + if (count($couchbase->getServers()) > 0) { + return true; + } + } catch (\CouchbaseException $e) { + return false; + } + + return true; + } + + public function getCassandraStorage() + { + if (!$this->cassandra) { + if (class_exists('phpcassa\ColumnFamily')) { + $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_test', array('127.0.0.1:9160')); + if ($this->testCassandraConnection($cassandra)) { + $this->removeCassandraDb(); + $this->cassandra = new Cassandra($cassandra); + $this->createCassandraDb($this->cassandra); + } else { + $this->cassandra = new NullStorage('Cassandra', 'Unable to connect to cassandra server on "127.0.0.1:9160"'); + } + } else { + $this->cassandra = new NullStorage('Cassandra', 'Missing cassandra library. Please run "composer.phar require thobbs/phpcassa:dev-master"'); + } + } + + return $this->cassandra; + } + + private function testCassandraConnection(\phpcassa\Connection\ConnectionPool $cassandra) + { + try { + new \phpcassa\SystemManager('localhost:9160'); + } catch (\Exception $e) { + return false; + } + + return true; + } + + private function removeCassandraDb() + { + $sys = new \phpcassa\SystemManager('localhost:9160'); + + try { + $sys->drop_keyspace('oauth2_test'); + } catch (\cassandra\InvalidRequestException $e) { + + } + } + + private function createCassandraDb(Cassandra $storage) + { + // create the cassandra keyspace and column family + $sys = new \phpcassa\SystemManager('localhost:9160'); + + $sys->create_keyspace('oauth2_test', array( + "strategy_class" => \phpcassa\Schema\StrategyClass::SIMPLE_STRATEGY, + "strategy_options" => array('replication_factor' => '1') + )); + + $sys->create_column_family('oauth2_test', 'auth'); + $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_test', array('127.0.0.1:9160')); + $cf = new \phpcassa\ColumnFamily($cassandra, 'auth'); + + // populate the data + $storage->setClientDetails("oauth_test_client", "testpass", "http://example.com", 'implicit password'); + $storage->setAccessToken("testtoken", "Some Client", '', time() + 1000); + $storage->setAuthorizationCode("testcode", "Some Client", '', '', time() + 1000); + + $storage->setScope('supportedscope1 supportedscope2 supportedscope3 supportedscope4'); + $storage->setScope('defaultscope1 defaultscope2', null, 'default'); + + $storage->setScope('clientscope1 clientscope2', 'Test Client ID'); + $storage->setScope('clientscope1 clientscope2', 'Test Client ID', 'default'); + + $storage->setScope('clientscope1 clientscope2 clientscope3', 'Test Client ID 2'); + $storage->setScope('clientscope1 clientscope2', 'Test Client ID 2', 'default'); + + $storage->setScope('clientscope1 clientscope2', 'Test Default Scope Client ID'); + $storage->setScope('clientscope1 clientscope2', 'Test Default Scope Client ID', 'default'); + + $storage->setScope('clientscope1 clientscope2 clientscope3', 'Test Default Scope Client ID 2'); + $storage->setScope('clientscope3', 'Test Default Scope Client ID 2', 'default'); + + $storage->setClientKey('oauth_test_client', $this->getTestPublicKey(), 'test_subject'); + + $cf->insert("oauth_public_keys:ClientID_One", array('__data' => json_encode(array("public_key" => "client_1_public", "private_key" => "client_1_private", "encryption_algorithm" => "RS256")))); + $cf->insert("oauth_public_keys:ClientID_Two", array('__data' => json_encode(array("public_key" => "client_2_public", "private_key" => "client_2_private", "encryption_algorithm" => "RS256")))); + $cf->insert("oauth_public_keys:", array('__data' => json_encode(array("public_key" => $this->getTestPublicKey(), "private_key" => $this->getTestPrivateKey(), "encryption_algorithm" => "RS256")))); + + $cf->insert("oauth_users:testuser", array('__data' =>json_encode(array("password" => "password", "email" => "testuser@test.com", "email_verified" => true)))); + + } + + private function createSqliteDb(\PDO $pdo) + { + $this->runPdoSql($pdo); + } + + private function removeSqliteDb() + { + if (file_exists($this->getSqliteDir())) { + unlink($this->getSqliteDir()); + } + } + + private function createMysqlDb(\PDO $pdo) + { + $pdo->exec('CREATE DATABASE oauth2_server_php'); + $pdo->exec('USE oauth2_server_php'); + $this->runPdoSql($pdo); + } + + private function removeMysqlDb(\PDO $pdo) + { + $pdo->exec('DROP DATABASE IF EXISTS oauth2_server_php'); + } + + private function createPostgresDb() + { + if (!`psql postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname='postgres'"`) { + `createuser -s -r postgres`; + } + + `createdb -O postgres oauth2_server_php`; + } + + private function populatePostgresDb(\PDO $pdo) + { + $this->runPdoSql($pdo); + } + + private function removePostgresDb() + { + if (trim(`psql -l | grep oauth2_server_php | wc -l`)) { + `dropdb oauth2_server_php`; + } + } + + public function runPdoSql(\PDO $pdo) + { + $storage = new Pdo($pdo); + foreach (explode(';', $storage->getBuildSql()) as $statement) { + $result = $pdo->exec($statement); + } + + // set up scopes + $sql = 'INSERT INTO oauth_scopes (scope) VALUES (?)'; + foreach (explode(' ', 'supportedscope1 supportedscope2 supportedscope3 supportedscope4 clientscope1 clientscope2 clientscope3') as $supportedScope) { + $pdo->prepare($sql)->execute(array($supportedScope)); + } + + $sql = 'INSERT INTO oauth_scopes (scope, is_default) VALUES (?, ?)'; + foreach (array('defaultscope1', 'defaultscope2') as $defaultScope) { + $pdo->prepare($sql)->execute(array($defaultScope, true)); + } + + // set up clients + $sql = 'INSERT INTO oauth_clients (client_id, client_secret, scope, grant_types) VALUES (?, ?, ?, ?)'; + $pdo->prepare($sql)->execute(array('Test Client ID', 'TestSecret', 'clientscope1 clientscope2', null)); + $pdo->prepare($sql)->execute(array('Test Client ID 2', 'TestSecret', 'clientscope1 clientscope2 clientscope3', null)); + $pdo->prepare($sql)->execute(array('Test Default Scope Client ID', 'TestSecret', 'clientscope1 clientscope2', null)); + $pdo->prepare($sql)->execute(array('oauth_test_client', 'testpass', null, 'implicit password')); + + // set up misc + $sql = 'INSERT INTO oauth_access_tokens (access_token, client_id, expires, user_id) VALUES (?, ?, ?, ?)'; + $pdo->prepare($sql)->execute(array('testtoken', 'Some Client', date('Y-m-d H:i:s', strtotime('+1 hour')), null)); + $pdo->prepare($sql)->execute(array('accesstoken-openid-connect', 'Some Client', date('Y-m-d H:i:s', strtotime('+1 hour')), 'testuser')); + + $sql = 'INSERT INTO oauth_authorization_codes (authorization_code, client_id, expires) VALUES (?, ?, ?)'; + $pdo->prepare($sql)->execute(array('testcode', 'Some Client', date('Y-m-d H:i:s', strtotime('+1 hour')))); + + $sql = 'INSERT INTO oauth_users (username, password, email, email_verified) VALUES (?, ?, ?, ?)'; + $pdo->prepare($sql)->execute(array('testuser', 'password', 'testuser@test.com', true)); + + $sql = 'INSERT INTO oauth_public_keys (client_id, public_key, private_key, encryption_algorithm) VALUES (?, ?, ?, ?)'; + $pdo->prepare($sql)->execute(array('ClientID_One', 'client_1_public', 'client_1_private', 'RS256')); + $pdo->prepare($sql)->execute(array('ClientID_Two', 'client_2_public', 'client_2_private', 'RS256')); + + $sql = 'INSERT INTO oauth_public_keys (client_id, public_key, private_key, encryption_algorithm) VALUES (?, ?, ?, ?)'; + $pdo->prepare($sql)->execute(array(null, $this->getTestPublicKey(), $this->getTestPrivateKey(), 'RS256')); + + $sql = 'INSERT INTO oauth_jwt (client_id, subject, public_key) VALUES (?, ?, ?)'; + $pdo->prepare($sql)->execute(array('oauth_test_client', 'test_subject', $this->getTestPublicKey())); + } + + public function getSqliteDir() + { + return $this->configDir. '/test.sqlite'; + } + + public function getConfigDir() + { + return $this->configDir; + } + + private function createCouchbaseDB(\Couchbase $db) + { + $db->set('oauth_clients-oauth_test_client',json_encode(array( + 'client_id' => "oauth_test_client", + 'client_secret' => "testpass", + 'redirect_uri' => "http://example.com", + 'grant_types' => 'implicit password' + ))); + + $db->set('oauth_access_tokens-testtoken',json_encode(array( + 'access_token' => "testtoken", + 'client_id' => "Some Client" + ))); + + $db->set('oauth_authorization_codes-testcode',json_encode(array( + 'access_token' => "testcode", + 'client_id' => "Some Client" + ))); + + $db->set('oauth_users-testuser',json_encode(array( + 'username' => 'testuser', + 'password' => 'password', + 'email' => 'testuser@test.com', + 'email_verified' => true, + ))); + + $db->set('oauth_jwt-oauth_test_client',json_encode(array( + 'client_id' => 'oauth_test_client', + 'key' => $this->getTestPublicKey(), + 'subject' => 'test_subject', + ))); + } + + private function clearCouchbase(\Couchbase $cb) + { + $cb->delete('oauth_authorization_codes-new-openid-code'); + $cb->delete('oauth_access_tokens-newtoken'); + $cb->delete('oauth_authorization_codes-newcode'); + $cb->delete('oauth_refresh_tokens-refreshtoken'); + } + + private function createMongo(\MongoDB $db) + { + $db->oauth_clients->insert(array( + 'client_id' => "oauth_test_client", + 'client_secret' => "testpass", + 'redirect_uri' => "http://example.com", + 'grant_types' => 'implicit password' + )); + + $db->oauth_access_tokens->insert(array( + 'access_token' => "testtoken", + 'client_id' => "Some Client" + )); + + $db->oauth_authorization_codes->insert(array( + 'authorization_code' => "testcode", + 'client_id' => "Some Client" + )); + + $db->oauth_users->insert(array( + 'username' => 'testuser', + 'password' => 'password', + 'email' => 'testuser@test.com', + 'email_verified' => true, + )); + + $db->oauth_keys->insert(array( + 'client_id' => null, + 'public_key' => $this->getTestPublicKey(), + 'private_key' => $this->getTestPrivateKey(), + 'encryption_algorithm' => 'RS256' + )); + + $db->oauth_jwt->insert(array( + 'client_id' => 'oauth_test_client', + 'key' => $this->getTestPublicKey(), + 'subject' => 'test_subject', + )); + } + + public function removeMongo(\MongoDB $db) + { + $db->drop(); + } + + private function createMongoDB(\MongoDB\Database $db) + { + $db->oauth_clients->insertOne(array( + 'client_id' => "oauth_test_client", + 'client_secret' => "testpass", + 'redirect_uri' => "http://example.com", + 'grant_types' => 'implicit password' + )); + + $db->oauth_access_tokens->insertOne(array( + 'access_token' => "testtoken", + 'client_id' => "Some Client" + )); + + $db->oauth_authorization_codes->insertOne(array( + 'authorization_code' => "testcode", + 'client_id' => "Some Client" + )); + + $db->oauth_users->insertOne(array( + 'username' => 'testuser', + 'password' => 'password', + 'email' => 'testuser@test.com', + 'email_verified' => true, + )); + + $db->oauth_keys->insertOne(array( + 'client_id' => null, + 'public_key' => $this->getTestPublicKey(), + 'private_key' => $this->getTestPrivateKey(), + 'encryption_algorithm' => 'RS256' + )); + + $db->oauth_jwt->insertOne(array( + 'client_id' => 'oauth_test_client', + 'key' => $this->getTestPublicKey(), + 'subject' => 'test_subject', + )); + } + + public function removeMongoDB(\MongoDB\Database $db) + { + $db->drop(); + } + + private function createRedisDb(Redis $storage) + { + $storage->setClientDetails("oauth_test_client", "testpass", "http://example.com", 'implicit password'); + $storage->setAccessToken("testtoken", "Some Client", '', time() + 1000); + $storage->setAuthorizationCode("testcode", "Some Client", '', '', time() + 1000); + $storage->setUser("testuser", "password"); + + $storage->setScope('supportedscope1 supportedscope2 supportedscope3 supportedscope4'); + $storage->setScope('defaultscope1 defaultscope2', null, 'default'); + + $storage->setScope('clientscope1 clientscope2', 'Test Client ID'); + $storage->setScope('clientscope1 clientscope2', 'Test Client ID', 'default'); + + $storage->setScope('clientscope1 clientscope2 clientscope3', 'Test Client ID 2'); + $storage->setScope('clientscope1 clientscope2', 'Test Client ID 2', 'default'); + + $storage->setScope('clientscope1 clientscope2', 'Test Default Scope Client ID'); + $storage->setScope('clientscope1 clientscope2', 'Test Default Scope Client ID', 'default'); + + $storage->setScope('clientscope1 clientscope2 clientscope3', 'Test Default Scope Client ID 2'); + $storage->setScope('clientscope3', 'Test Default Scope Client ID 2', 'default'); + + $storage->setClientKey('oauth_test_client', $this->getTestPublicKey(), 'test_subject'); + } + + public function getTestPublicKey() + { + return file_get_contents(__DIR__.'/../../../config/keys/id_rsa.pub'); + } + + private function getTestPrivateKey() + { + return file_get_contents(__DIR__.'/../../../config/keys/id_rsa'); + } + + public function getDynamoDbStorage() + { + if (!$this->dynamodb) { + // only run once per travis build + if (true == $this->getEnvVar('TRAVIS')) { + if (self::DYNAMODB_PHP_VERSION != $this->getEnvVar('TRAVIS_PHP_VERSION')) { + $this->dynamodb = new NullStorage('DynamoDb', 'Skipping for travis.ci - only run once per build'); + + return; + } + } + if (class_exists('\Aws\DynamoDb\DynamoDbClient')) { + if ($client = $this->getDynamoDbClient()) { + // travis runs a unique set of tables per build, to avoid conflict + $prefix = ''; + if ($build_id = $this->getEnvVar('TRAVIS_JOB_NUMBER')) { + $prefix = sprintf('build_%s_', $build_id); + } else { + if (!$this->deleteDynamoDb($client, $prefix, true)) { + return $this->dynamodb = new NullStorage('DynamoDb', 'Timed out while waiting for DynamoDB deletion (30 seconds)'); + } + } + $this->createDynamoDb($client, $prefix); + $this->populateDynamoDb($client, $prefix); + $config = array( + 'client_table' => $prefix.'oauth_clients', + 'access_token_table' => $prefix.'oauth_access_tokens', + 'refresh_token_table' => $prefix.'oauth_refresh_tokens', + 'code_table' => $prefix.'oauth_authorization_codes', + 'user_table' => $prefix.'oauth_users', + 'jwt_table' => $prefix.'oauth_jwt', + 'scope_table' => $prefix.'oauth_scopes', + 'public_key_table' => $prefix.'oauth_public_keys', + ); + $this->dynamodb = new DynamoDB($client, $config); + } elseif (!$this->dynamodb) { + $this->dynamodb = new NullStorage('DynamoDb', 'unable to connect to DynamoDB'); + } + } else { + $this->dynamodb = new NullStorage('DynamoDb', 'Missing DynamoDB library. Please run "composer.phar require aws/aws-sdk-php:dev-master'); + } + } + + return $this->dynamodb; + } + + private function getDynamoDbClient() + { + $config = array(); + // check for environment variables + if (($key = $this->getEnvVar('AWS_ACCESS_KEY_ID')) && ($secret = $this->getEnvVar('AWS_SECRET_KEY'))) { + $config['key'] = $key; + $config['secret'] = $secret; + } else { + // fall back on ~/.aws/credentials file + // @see http://docs.aws.amazon.com/aws-sdk-php/guide/latest/credentials.html#credential-profiles + if (!file_exists($this->getEnvVar('HOME') . '/.aws/credentials')) { + $this->dynamodb = new NullStorage('DynamoDb', 'No aws credentials file found, and no AWS_ACCESS_KEY_ID or AWS_SECRET_KEY environment variable set'); + + return; + } + + // set profile in AWS_PROFILE environment variable, defaults to "default" + $config['profile'] = $this->getEnvVar('AWS_PROFILE', 'default'); + } + + // set region in AWS_REGION environment variable, defaults to "us-east-1" + $config['region'] = $this->getEnvVar('AWS_REGION', \Aws\Common\Enum\Region::US_EAST_1); + + return \Aws\DynamoDb\DynamoDbClient::factory($config); + } + + private function deleteDynamoDb(\Aws\DynamoDb\DynamoDbClient $client, $prefix = null, $waitForDeletion = false) + { + $tablesList = explode(' ', 'oauth_access_tokens oauth_authorization_codes oauth_clients oauth_jwt oauth_public_keys oauth_refresh_tokens oauth_scopes oauth_users'); + $nbTables = count($tablesList); + + // Delete all table. + foreach ($tablesList as $key => $table) { + try { + $client->deleteTable(array('TableName' => $prefix.$table)); + } catch (\Aws\DynamoDb\Exception\DynamoDbException $e) { + // Table does not exist : nothing to do + } + } + + // Wait for deleting + if ($waitForDeletion) { + $retries = 5; + $nbTableDeleted = 0; + while ($nbTableDeleted != $nbTables) { + $nbTableDeleted = 0; + foreach ($tablesList as $key => $table) { + try { + $result = $client->describeTable(array('TableName' => $prefix.$table)); + } catch (\Aws\DynamoDb\Exception\DynamoDbException $e) { + // Table does not exist : nothing to do + $nbTableDeleted++; + } + } + if ($nbTableDeleted != $nbTables) { + if ($retries < 0) { + // we are tired of waiting + return false; + } + sleep(5); + echo "Sleeping 5 seconds for DynamoDB ($retries more retries)...\n"; + $retries--; + } + } + } + + return true; + } + + private function createDynamoDb(\Aws\DynamoDb\DynamoDbClient $client, $prefix = null) + { + $tablesList = explode(' ', 'oauth_access_tokens oauth_authorization_codes oauth_clients oauth_jwt oauth_public_keys oauth_refresh_tokens oauth_scopes oauth_users'); + $nbTables = count($tablesList); + $client->createTable(array( + 'TableName' => $prefix.'oauth_access_tokens', + 'AttributeDefinitions' => array( + array('AttributeName' => 'access_token','AttributeType' => 'S') + ), + 'KeySchema' => array(array('AttributeName' => 'access_token','KeyType' => 'HASH')), + 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) + )); + + $client->createTable(array( + 'TableName' => $prefix.'oauth_authorization_codes', + 'AttributeDefinitions' => array( + array('AttributeName' => 'authorization_code','AttributeType' => 'S') + ), + 'KeySchema' => array(array('AttributeName' => 'authorization_code','KeyType' => 'HASH')), + 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) + )); + + $client->createTable(array( + 'TableName' => $prefix.'oauth_clients', + 'AttributeDefinitions' => array( + array('AttributeName' => 'client_id','AttributeType' => 'S') + ), + 'KeySchema' => array(array('AttributeName' => 'client_id','KeyType' => 'HASH')), + 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) + )); + + $client->createTable(array( + 'TableName' => $prefix.'oauth_jwt', + 'AttributeDefinitions' => array( + array('AttributeName' => 'client_id','AttributeType' => 'S'), + array('AttributeName' => 'subject','AttributeType' => 'S') + ), + 'KeySchema' => array( + array('AttributeName' => 'client_id','KeyType' => 'HASH'), + array('AttributeName' => 'subject','KeyType' => 'RANGE') + ), + 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) + )); + + $client->createTable(array( + 'TableName' => $prefix.'oauth_public_keys', + 'AttributeDefinitions' => array( + array('AttributeName' => 'client_id','AttributeType' => 'S') + ), + 'KeySchema' => array(array('AttributeName' => 'client_id','KeyType' => 'HASH')), + 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) + )); + + $client->createTable(array( + 'TableName' => $prefix.'oauth_refresh_tokens', + 'AttributeDefinitions' => array( + array('AttributeName' => 'refresh_token','AttributeType' => 'S') + ), + 'KeySchema' => array(array('AttributeName' => 'refresh_token','KeyType' => 'HASH')), + 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) + )); + + $client->createTable(array( + 'TableName' => $prefix.'oauth_scopes', + 'AttributeDefinitions' => array( + array('AttributeName' => 'scope','AttributeType' => 'S'), + array('AttributeName' => 'is_default','AttributeType' => 'S') + ), + 'KeySchema' => array(array('AttributeName' => 'scope','KeyType' => 'HASH')), + 'GlobalSecondaryIndexes' => array( + array( + 'IndexName' => 'is_default-index', + 'KeySchema' => array(array('AttributeName' => 'is_default', 'KeyType' => 'HASH')), + 'Projection' => array('ProjectionType' => 'ALL'), + 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) + ), + ), + 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) + )); + + $client->createTable(array( + 'TableName' => $prefix.'oauth_users', + 'AttributeDefinitions' => array(array('AttributeName' => 'username','AttributeType' => 'S')), + 'KeySchema' => array(array('AttributeName' => 'username','KeyType' => 'HASH')), + 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) + )); + + // Wait for creation + $nbTableCreated = 0; + while ($nbTableCreated != $nbTables) { + $nbTableCreated = 0; + foreach ($tablesList as $key => $table) { + try { + $result = $client->describeTable(array('TableName' => $prefix.$table)); + if ($result['Table']['TableStatus'] == 'ACTIVE') { + $nbTableCreated++; + } + } catch (\Aws\DynamoDb\Exception\DynamoDbException $e) { + // Table does not exist : nothing to do + $nbTableCreated++; + } + } + if ($nbTableCreated != $nbTables) { + sleep(1); + } + } + } + + private function populateDynamoDb($client, $prefix = null) + { + // set up scopes + foreach (explode(' ', 'supportedscope1 supportedscope2 supportedscope3 supportedscope4 clientscope1 clientscope2 clientscope3') as $supportedScope) { + $client->putItem(array( + 'TableName' => $prefix.'oauth_scopes', + 'Item' => array('scope' => array('S' => $supportedScope)) + )); + } + + foreach (array('defaultscope1', 'defaultscope2') as $defaultScope) { + $client->putItem(array( + 'TableName' => $prefix.'oauth_scopes', + 'Item' => array('scope' => array('S' => $defaultScope), 'is_default' => array('S' => "true")) + )); + } + + $client->putItem(array( + 'TableName' => $prefix.'oauth_clients', + 'Item' => array( + 'client_id' => array('S' => 'Test Client ID'), + 'client_secret' => array('S' => 'TestSecret'), + 'scope' => array('S' => 'clientscope1 clientscope2') + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_clients', + 'Item' => array( + 'client_id' => array('S' => 'Test Client ID 2'), + 'client_secret' => array('S' => 'TestSecret'), + 'scope' => array('S' => 'clientscope1 clientscope2 clientscope3') + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_clients', + 'Item' => array( + 'client_id' => array('S' => 'Test Default Scope Client ID'), + 'client_secret' => array('S' => 'TestSecret'), + 'scope' => array('S' => 'clientscope1 clientscope2') + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_clients', + 'Item' => array( + 'client_id' => array('S' => 'oauth_test_client'), + 'client_secret' => array('S' => 'testpass'), + 'grant_types' => array('S' => 'implicit password') + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_access_tokens', + 'Item' => array( + 'access_token' => array('S' => 'testtoken'), + 'client_id' => array('S' => 'Some Client'), + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_access_tokens', + 'Item' => array( + 'access_token' => array('S' => 'accesstoken-openid-connect'), + 'client_id' => array('S' => 'Some Client'), + 'user_id' => array('S' => 'testuser'), + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_authorization_codes', + 'Item' => array( + 'authorization_code' => array('S' => 'testcode'), + 'client_id' => array('S' => 'Some Client'), + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_users', + 'Item' => array( + 'username' => array('S' => 'testuser'), + 'password' => array('S' => 'password'), + 'email' => array('S' => 'testuser@test.com'), + 'email_verified' => array('S' => 'true'), + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_public_keys', + 'Item' => array( + 'client_id' => array('S' => 'ClientID_One'), + 'public_key' => array('S' => 'client_1_public'), + 'private_key' => array('S' => 'client_1_private'), + 'encryption_algorithm' => array('S' => 'RS256'), + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_public_keys', + 'Item' => array( + 'client_id' => array('S' => 'ClientID_Two'), + 'public_key' => array('S' => 'client_2_public'), + 'private_key' => array('S' => 'client_2_private'), + 'encryption_algorithm' => array('S' => 'RS256'), + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_public_keys', + 'Item' => array( + 'client_id' => array('S' => '0'), + 'public_key' => array('S' => $this->getTestPublicKey()), + 'private_key' => array('S' => $this->getTestPrivateKey()), + 'encryption_algorithm' => array('S' => 'RS256'), + ) + )); + + $client->putItem(array( + 'TableName' => $prefix.'oauth_jwt', + 'Item' => array( + 'client_id' => array('S' => 'oauth_test_client'), + 'subject' => array('S' => 'test_subject'), + 'public_key' => array('S' => $this->getTestPublicKey()), + ) + )); + } + + public function cleanupTravisDynamoDb($prefix = null) + { + if (is_null($prefix)) { + // skip this when not applicable + if (!$this->getEnvVar('TRAVIS') || self::DYNAMODB_PHP_VERSION != $this->getEnvVar('TRAVIS_PHP_VERSION')) { + return; + } + + $prefix = sprintf('build_%s_', $this->getEnvVar('TRAVIS_JOB_NUMBER')); + } + + $client = $this->getDynamoDbClient(); + $this->deleteDynamoDb($client, $prefix); + } + + private function getEnvVar($var, $default = null) + { + return isset($_SERVER[$var]) ? $_SERVER[$var] : (getenv($var) ?: $default); + } +} diff --git a/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/NullStorage.php b/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/NullStorage.php new file mode 100644 index 000000000..6caa62068 --- /dev/null +++ b/vendor/bshaffer/oauth2-server-php/test/lib/OAuth2/Storage/NullStorage.php @@ -0,0 +1,32 @@ +<?php + +namespace OAuth2\Storage; + +/** +* +*/ +class NullStorage extends Memory +{ + private $name; + private $description; + + public function __construct($name, $description = null) + { + $this->name = $name; + $this->description = $description; + } + + public function __toString() + { + return $this->name; + } + + public function getMessage() + { + if ($this->description) { + return $this->description; + } + + return $this->name; + } +} diff --git a/vendor/composer/ClassLoader.php b/vendor/composer/ClassLoader.php index ac67d302a..2c72175e7 100644 --- a/vendor/composer/ClassLoader.php +++ b/vendor/composer/ClassLoader.php @@ -55,6 +55,7 @@ class ClassLoader private $classMap = array(); private $classMapAuthoritative = false; private $missingClasses = array(); + private $apcuPrefix; public function getPrefixes() { @@ -272,6 +273,26 @@ class ClassLoader } /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** * Registers this instance as an autoloader. * * @param bool $prepend Whether to prepend the autoloader or not @@ -313,11 +334,6 @@ class ClassLoader */ public function findFile($class) { - // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731 - if ('\\' == $class[0]) { - $class = substr($class, 1); - } - // class map lookup if (isset($this->classMap[$class])) { return $this->classMap[$class]; @@ -325,6 +341,12 @@ class ClassLoader if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { return false; } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } $file = $this->findFileWithExtension($class, '.php'); @@ -333,6 +355,10 @@ class ClassLoader $file = $this->findFileWithExtension($class, '.hh'); } + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + if (false === $file) { // Remember that this class does not exist. $this->missingClasses[$class] = true; @@ -348,9 +374,13 @@ class ClassLoader $first = $class[0]; if (isset($this->prefixLengthsPsr4[$first])) { - foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { - if (0 === strpos($class, $prefix)) { - foreach ($this->prefixDirsPsr4[$prefix] as $dir) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath.'\\'; + if (isset($this->prefixDirsPsr4[$search])) { + foreach ($this->prefixDirsPsr4[$search] as $dir) { + $length = $this->prefixLengthsPsr4[$first][$search]; if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { return $file; } diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE index 1a2812488..f27399a04 100644 --- a/vendor/composer/LICENSE +++ b/vendor/composer/LICENSE @@ -1,5 +1,5 @@ -Copyright (c) 2016 Nils Adermann, Jordi Boggiano +Copyright (c) Nils Adermann, Jordi Boggiano Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 54b09f68b..5899fb9ed 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -6,7 +6,315 @@ $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( + 'HTMLPurifier' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.php', + 'HTMLPurifier_Arborize' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Arborize.php', + 'HTMLPurifier_AttrCollections' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrCollections.php', + 'HTMLPurifier_AttrDef' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef.php', + 'HTMLPurifier_AttrDef_CSS' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS.php', + 'HTMLPurifier_AttrDef_CSS_AlphaValue' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/AlphaValue.php', + 'HTMLPurifier_AttrDef_CSS_Background' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Background.php', + 'HTMLPurifier_AttrDef_CSS_BackgroundPosition' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php', + 'HTMLPurifier_AttrDef_CSS_Border' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Border.php', + 'HTMLPurifier_AttrDef_CSS_Color' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Color.php', + 'HTMLPurifier_AttrDef_CSS_Composite' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Composite.php', + 'HTMLPurifier_AttrDef_CSS_DenyElementDecorator' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php', + 'HTMLPurifier_AttrDef_CSS_Filter' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Filter.php', + 'HTMLPurifier_AttrDef_CSS_Font' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Font.php', + 'HTMLPurifier_AttrDef_CSS_FontFamily' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/FontFamily.php', + 'HTMLPurifier_AttrDef_CSS_Ident' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ident.php', + 'HTMLPurifier_AttrDef_CSS_ImportantDecorator' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/ImportantDecorator.php', + 'HTMLPurifier_AttrDef_CSS_Length' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Length.php', + 'HTMLPurifier_AttrDef_CSS_ListStyle' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/ListStyle.php', + 'HTMLPurifier_AttrDef_CSS_Multiple' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Multiple.php', + 'HTMLPurifier_AttrDef_CSS_Number' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Number.php', + 'HTMLPurifier_AttrDef_CSS_Percentage' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Percentage.php', + 'HTMLPurifier_AttrDef_CSS_TextDecoration' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/TextDecoration.php', + 'HTMLPurifier_AttrDef_CSS_URI' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/URI.php', + 'HTMLPurifier_AttrDef_Clone' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Clone.php', + 'HTMLPurifier_AttrDef_Enum' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Enum.php', + 'HTMLPurifier_AttrDef_HTML_Bool' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Bool.php', + 'HTMLPurifier_AttrDef_HTML_Class' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Class.php', + 'HTMLPurifier_AttrDef_HTML_Color' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Color.php', + 'HTMLPurifier_AttrDef_HTML_FrameTarget' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/FrameTarget.php', + 'HTMLPurifier_AttrDef_HTML_ID' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/ID.php', + 'HTMLPurifier_AttrDef_HTML_Length' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Length.php', + 'HTMLPurifier_AttrDef_HTML_LinkTypes' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/LinkTypes.php', + 'HTMLPurifier_AttrDef_HTML_MultiLength' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/MultiLength.php', + 'HTMLPurifier_AttrDef_HTML_Nmtokens' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Nmtokens.php', + 'HTMLPurifier_AttrDef_HTML_Pixels' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Pixels.php', + 'HTMLPurifier_AttrDef_Integer' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Integer.php', + 'HTMLPurifier_AttrDef_Lang' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Lang.php', + 'HTMLPurifier_AttrDef_Switch' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Switch.php', + 'HTMLPurifier_AttrDef_Text' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Text.php', + 'HTMLPurifier_AttrDef_URI' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI.php', + 'HTMLPurifier_AttrDef_URI_Email' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Email.php', + 'HTMLPurifier_AttrDef_URI_Email_SimpleCheck' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Email/SimpleCheck.php', + 'HTMLPurifier_AttrDef_URI_Host' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php', + 'HTMLPurifier_AttrDef_URI_IPv4' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv4.php', + 'HTMLPurifier_AttrDef_URI_IPv6' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv6.php', + 'HTMLPurifier_AttrTransform' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform.php', + 'HTMLPurifier_AttrTransform_Background' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Background.php', + 'HTMLPurifier_AttrTransform_BdoDir' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BdoDir.php', + 'HTMLPurifier_AttrTransform_BgColor' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BgColor.php', + 'HTMLPurifier_AttrTransform_BoolToCSS' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BoolToCSS.php', + 'HTMLPurifier_AttrTransform_Border' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Border.php', + 'HTMLPurifier_AttrTransform_EnumToCSS' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/EnumToCSS.php', + 'HTMLPurifier_AttrTransform_ImgRequired' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgRequired.php', + 'HTMLPurifier_AttrTransform_ImgSpace' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgSpace.php', + 'HTMLPurifier_AttrTransform_Input' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Input.php', + 'HTMLPurifier_AttrTransform_Lang' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Lang.php', + 'HTMLPurifier_AttrTransform_Length' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Length.php', + 'HTMLPurifier_AttrTransform_Name' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Name.php', + 'HTMLPurifier_AttrTransform_NameSync' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/NameSync.php', + 'HTMLPurifier_AttrTransform_Nofollow' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Nofollow.php', + 'HTMLPurifier_AttrTransform_SafeEmbed' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeEmbed.php', + 'HTMLPurifier_AttrTransform_SafeObject' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeObject.php', + 'HTMLPurifier_AttrTransform_SafeParam' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeParam.php', + 'HTMLPurifier_AttrTransform_ScriptRequired' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ScriptRequired.php', + 'HTMLPurifier_AttrTransform_TargetBlank' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetBlank.php', + 'HTMLPurifier_AttrTransform_TargetNoopener' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetNoopener.php', + 'HTMLPurifier_AttrTransform_TargetNoreferrer' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetNoreferrer.php', + 'HTMLPurifier_AttrTransform_Textarea' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Textarea.php', + 'HTMLPurifier_AttrTypes' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTypes.php', + 'HTMLPurifier_AttrValidator' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrValidator.php', + 'HTMLPurifier_Bootstrap' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Bootstrap.php', + 'HTMLPurifier_CSSDefinition' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/CSSDefinition.php', + 'HTMLPurifier_ChildDef' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef.php', + 'HTMLPurifier_ChildDef_Chameleon' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Chameleon.php', + 'HTMLPurifier_ChildDef_Custom' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Custom.php', + 'HTMLPurifier_ChildDef_Empty' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Empty.php', + 'HTMLPurifier_ChildDef_List' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/List.php', + 'HTMLPurifier_ChildDef_Optional' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Optional.php', + 'HTMLPurifier_ChildDef_Required' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Required.php', + 'HTMLPurifier_ChildDef_StrictBlockquote' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/StrictBlockquote.php', + 'HTMLPurifier_ChildDef_Table' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Table.php', + 'HTMLPurifier_Config' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Config.php', + 'HTMLPurifier_ConfigSchema' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema.php', + 'HTMLPurifier_ConfigSchema_Builder_ConfigSchema' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php', + 'HTMLPurifier_ConfigSchema_Builder_Xml' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/Xml.php', + 'HTMLPurifier_ConfigSchema_Exception' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Exception.php', + 'HTMLPurifier_ConfigSchema_Interchange' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange.php', + 'HTMLPurifier_ConfigSchema_InterchangeBuilder' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/InterchangeBuilder.php', + 'HTMLPurifier_ConfigSchema_Interchange_Directive' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Directive.php', + 'HTMLPurifier_ConfigSchema_Interchange_Id' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Id.php', + 'HTMLPurifier_ConfigSchema_Validator' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Validator.php', + 'HTMLPurifier_ConfigSchema_ValidatorAtom' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/ValidatorAtom.php', + 'HTMLPurifier_ContentSets' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ContentSets.php', + 'HTMLPurifier_Context' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Context.php', + 'HTMLPurifier_Definition' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Definition.php', + 'HTMLPurifier_DefinitionCache' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache.php', + 'HTMLPurifier_DefinitionCacheFactory' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCacheFactory.php', + 'HTMLPurifier_DefinitionCache_Decorator' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator.php', + 'HTMLPurifier_DefinitionCache_Decorator_Cleanup' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php', + 'HTMLPurifier_DefinitionCache_Decorator_Memory' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Memory.php', + 'HTMLPurifier_DefinitionCache_Null' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Null.php', + 'HTMLPurifier_DefinitionCache_Serializer' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer.php', + 'HTMLPurifier_Doctype' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Doctype.php', + 'HTMLPurifier_DoctypeRegistry' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/DoctypeRegistry.php', + 'HTMLPurifier_ElementDef' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ElementDef.php', + 'HTMLPurifier_Encoder' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Encoder.php', + 'HTMLPurifier_EntityLookup' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup.php', + 'HTMLPurifier_EntityParser' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/EntityParser.php', + 'HTMLPurifier_ErrorCollector' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ErrorCollector.php', + 'HTMLPurifier_ErrorStruct' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/ErrorStruct.php', + 'HTMLPurifier_Exception' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Exception.php', + 'HTMLPurifier_Filter' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Filter.php', + 'HTMLPurifier_Filter_ExtractStyleBlocks' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Filter/ExtractStyleBlocks.php', + 'HTMLPurifier_Filter_YouTube' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Filter/YouTube.php', + 'HTMLPurifier_Generator' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Generator.php', + 'HTMLPurifier_HTMLDefinition' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLDefinition.php', + 'HTMLPurifier_HTMLModule' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule.php', + 'HTMLPurifier_HTMLModuleManager' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModuleManager.php', + 'HTMLPurifier_HTMLModule_Bdo' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Bdo.php', + 'HTMLPurifier_HTMLModule_CommonAttributes' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/CommonAttributes.php', + 'HTMLPurifier_HTMLModule_Edit' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Edit.php', + 'HTMLPurifier_HTMLModule_Forms' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Forms.php', + 'HTMLPurifier_HTMLModule_Hypertext' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Hypertext.php', + 'HTMLPurifier_HTMLModule_Iframe' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Iframe.php', + 'HTMLPurifier_HTMLModule_Image' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Image.php', + 'HTMLPurifier_HTMLModule_Legacy' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Legacy.php', + 'HTMLPurifier_HTMLModule_List' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/List.php', + 'HTMLPurifier_HTMLModule_Name' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Name.php', + 'HTMLPurifier_HTMLModule_Nofollow' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Nofollow.php', + 'HTMLPurifier_HTMLModule_NonXMLCommonAttributes' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php', + 'HTMLPurifier_HTMLModule_Object' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Object.php', + 'HTMLPurifier_HTMLModule_Presentation' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Presentation.php', + 'HTMLPurifier_HTMLModule_Proprietary' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Proprietary.php', + 'HTMLPurifier_HTMLModule_Ruby' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Ruby.php', + 'HTMLPurifier_HTMLModule_SafeEmbed' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeEmbed.php', + 'HTMLPurifier_HTMLModule_SafeObject' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeObject.php', + 'HTMLPurifier_HTMLModule_SafeScripting' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeScripting.php', + 'HTMLPurifier_HTMLModule_Scripting' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Scripting.php', + 'HTMLPurifier_HTMLModule_StyleAttribute' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/StyleAttribute.php', + 'HTMLPurifier_HTMLModule_Tables' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tables.php', + 'HTMLPurifier_HTMLModule_Target' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Target.php', + 'HTMLPurifier_HTMLModule_TargetBlank' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetBlank.php', + 'HTMLPurifier_HTMLModule_TargetNoopener' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoopener.php', + 'HTMLPurifier_HTMLModule_TargetNoreferrer' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoreferrer.php', + 'HTMLPurifier_HTMLModule_Text' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Text.php', + 'HTMLPurifier_HTMLModule_Tidy' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy.php', + 'HTMLPurifier_HTMLModule_Tidy_Name' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Name.php', + 'HTMLPurifier_HTMLModule_Tidy_Proprietary' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Proprietary.php', + 'HTMLPurifier_HTMLModule_Tidy_Strict' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Strict.php', + 'HTMLPurifier_HTMLModule_Tidy_Transitional' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Transitional.php', + 'HTMLPurifier_HTMLModule_Tidy_XHTML' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/XHTML.php', + 'HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php', + 'HTMLPurifier_HTMLModule_XMLCommonAttributes' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/XMLCommonAttributes.php', + 'HTMLPurifier_IDAccumulator' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/IDAccumulator.php', + 'HTMLPurifier_Injector' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Injector.php', + 'HTMLPurifier_Injector_AutoParagraph' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Injector/AutoParagraph.php', + 'HTMLPurifier_Injector_DisplayLinkURI' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Injector/DisplayLinkURI.php', + 'HTMLPurifier_Injector_Linkify' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Injector/Linkify.php', + 'HTMLPurifier_Injector_PurifierLinkify' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Injector/PurifierLinkify.php', + 'HTMLPurifier_Injector_RemoveEmpty' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Injector/RemoveEmpty.php', + 'HTMLPurifier_Injector_RemoveSpansWithoutAttributes' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Injector/RemoveSpansWithoutAttributes.php', + 'HTMLPurifier_Injector_SafeObject' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Injector/SafeObject.php', + 'HTMLPurifier_Language' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Language.php', + 'HTMLPurifier_LanguageFactory' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/LanguageFactory.php', + 'HTMLPurifier_Language_en_x_test' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Language/classes/en-x-test.php', + 'HTMLPurifier_Length' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Length.php', + 'HTMLPurifier_Lexer' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Lexer.php', + 'HTMLPurifier_Lexer_DOMLex' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DOMLex.php', + 'HTMLPurifier_Lexer_DirectLex' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DirectLex.php', + 'HTMLPurifier_Lexer_PH5P' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/PH5P.php', + 'HTMLPurifier_Node' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Node.php', + 'HTMLPurifier_Node_Comment' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Node/Comment.php', + 'HTMLPurifier_Node_Element' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Node/Element.php', + 'HTMLPurifier_Node_Text' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Node/Text.php', + 'HTMLPurifier_PercentEncoder' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/PercentEncoder.php', + 'HTMLPurifier_Printer' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Printer.php', + 'HTMLPurifier_Printer_CSSDefinition' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Printer/CSSDefinition.php', + 'HTMLPurifier_Printer_ConfigForm' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.php', + 'HTMLPurifier_Printer_ConfigForm_NullDecorator' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.php', + 'HTMLPurifier_Printer_ConfigForm_bool' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.php', + 'HTMLPurifier_Printer_ConfigForm_default' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.php', + 'HTMLPurifier_Printer_HTMLDefinition' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Printer/HTMLDefinition.php', + 'HTMLPurifier_PropertyList' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/PropertyList.php', + 'HTMLPurifier_PropertyListIterator' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/PropertyListIterator.php', + 'HTMLPurifier_Queue' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Queue.php', + 'HTMLPurifier_Strategy' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Strategy.php', + 'HTMLPurifier_Strategy_Composite' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/Composite.php', + 'HTMLPurifier_Strategy_Core' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/Core.php', + 'HTMLPurifier_Strategy_FixNesting' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/FixNesting.php', + 'HTMLPurifier_Strategy_MakeWellFormed' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/MakeWellFormed.php', + 'HTMLPurifier_Strategy_RemoveForeignElements' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/RemoveForeignElements.php', + 'HTMLPurifier_Strategy_ValidateAttributes' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/ValidateAttributes.php', + 'HTMLPurifier_StringHash' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/StringHash.php', + 'HTMLPurifier_StringHashParser' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/StringHashParser.php', + 'HTMLPurifier_TagTransform' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/TagTransform.php', + 'HTMLPurifier_TagTransform_Font' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/TagTransform/Font.php', + 'HTMLPurifier_TagTransform_Simple' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/TagTransform/Simple.php', + 'HTMLPurifier_Token' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Token.php', + 'HTMLPurifier_TokenFactory' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/TokenFactory.php', + 'HTMLPurifier_Token_Comment' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Token/Comment.php', + 'HTMLPurifier_Token_Empty' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Token/Empty.php', + 'HTMLPurifier_Token_End' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Token/End.php', + 'HTMLPurifier_Token_Start' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Token/Start.php', + 'HTMLPurifier_Token_Tag' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Token/Tag.php', + 'HTMLPurifier_Token_Text' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Token/Text.php', + 'HTMLPurifier_URI' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URI.php', + 'HTMLPurifier_URIDefinition' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIDefinition.php', + 'HTMLPurifier_URIFilter' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter.php', + 'HTMLPurifier_URIFilter_DisableExternal' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/DisableExternal.php', + 'HTMLPurifier_URIFilter_DisableExternalResources' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/DisableExternalResources.php', + 'HTMLPurifier_URIFilter_DisableResources' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/DisableResources.php', + 'HTMLPurifier_URIFilter_HostBlacklist' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/HostBlacklist.php', + 'HTMLPurifier_URIFilter_MakeAbsolute' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/MakeAbsolute.php', + 'HTMLPurifier_URIFilter_Munge' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/Munge.php', + 'HTMLPurifier_URIFilter_SafeIframe' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/SafeIframe.php', + 'HTMLPurifier_URIParser' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIParser.php', + 'HTMLPurifier_URIScheme' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme.php', + 'HTMLPurifier_URISchemeRegistry' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URISchemeRegistry.php', + 'HTMLPurifier_URIScheme_data' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/data.php', + 'HTMLPurifier_URIScheme_file' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/file.php', + 'HTMLPurifier_URIScheme_ftp' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/ftp.php', + 'HTMLPurifier_URIScheme_http' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/http.php', + 'HTMLPurifier_URIScheme_https' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/https.php', + 'HTMLPurifier_URIScheme_mailto' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/mailto.php', + 'HTMLPurifier_URIScheme_news' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/news.php', + 'HTMLPurifier_URIScheme_nntp' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/nntp.php', + 'HTMLPurifier_URIScheme_tel' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/tel.php', + 'HTMLPurifier_UnitConverter' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/UnitConverter.php', + 'HTMLPurifier_VarParser' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/VarParser.php', + 'HTMLPurifier_VarParserException' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/VarParserException.php', + 'HTMLPurifier_VarParser_Flexible' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/VarParser/Flexible.php', + 'HTMLPurifier_VarParser_Native' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/VarParser/Native.php', + 'HTMLPurifier_Zipper' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier/Zipper.php', 'Hubzilla\\Import\\Import' => $baseDir . '/include/Import/Importer.php', + 'Markdownify\\Converter' => $vendorDir . '/pixel418/markdownify/src/Converter.php', + 'Markdownify\\ConverterExtra' => $vendorDir . '/pixel418/markdownify/src/ConverterExtra.php', + 'Markdownify\\Parser' => $vendorDir . '/pixel418/markdownify/src/Parser.php', + 'Michelf\\Markdown' => $vendorDir . '/michelf/php-markdown/Michelf/Markdown.php', + 'Michelf\\MarkdownExtra' => $vendorDir . '/michelf/php-markdown/Michelf/MarkdownExtra.php', + 'Michelf\\MarkdownInterface' => $vendorDir . '/michelf/php-markdown/Michelf/MarkdownInterface.php', + 'OAuth2\\Autoloader' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Autoloader.php', + 'OAuth2\\ClientAssertionType\\ClientAssertionTypeInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php', + 'OAuth2\\ClientAssertionType\\HttpBasic' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/HttpBasic.php', + 'OAuth2\\Controller\\AuthorizeController' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeController.php', + 'OAuth2\\Controller\\AuthorizeControllerInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeControllerInterface.php', + 'OAuth2\\Controller\\ResourceController' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceController.php', + 'OAuth2\\Controller\\ResourceControllerInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceControllerInterface.php', + 'OAuth2\\Controller\\TokenController' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenController.php', + 'OAuth2\\Controller\\TokenControllerInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenControllerInterface.php', + 'OAuth2\\Encryption\\EncryptionInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Encryption/EncryptionInterface.php', + 'OAuth2\\Encryption\\FirebaseJwt' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Encryption/FirebaseJwt.php', + 'OAuth2\\Encryption\\Jwt' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Encryption/Jwt.php', + 'OAuth2\\GrantType\\AuthorizationCode' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/GrantType/AuthorizationCode.php', + 'OAuth2\\GrantType\\ClientCredentials' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/GrantType/ClientCredentials.php', + 'OAuth2\\GrantType\\GrantTypeInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/GrantType/GrantTypeInterface.php', + 'OAuth2\\GrantType\\JwtBearer' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/GrantType/JwtBearer.php', + 'OAuth2\\GrantType\\RefreshToken' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/GrantType/RefreshToken.php', + 'OAuth2\\GrantType\\UserCredentials' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/GrantType/UserCredentials.php', + 'OAuth2\\OpenID\\Controller\\AuthorizeController' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeController.php', + 'OAuth2\\OpenID\\Controller\\AuthorizeControllerInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php', + 'OAuth2\\OpenID\\Controller\\UserInfoController' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/UserInfoController.php', + 'OAuth2\\OpenID\\Controller\\UserInfoControllerInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php', + 'OAuth2\\OpenID\\GrantType\\AuthorizationCode' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/GrantType/AuthorizationCode.php', + 'OAuth2\\OpenID\\ResponseType\\AuthorizationCode' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php', + 'OAuth2\\OpenID\\ResponseType\\AuthorizationCodeInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php', + 'OAuth2\\OpenID\\ResponseType\\CodeIdToken' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdToken.php', + 'OAuth2\\OpenID\\ResponseType\\CodeIdTokenInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php', + 'OAuth2\\OpenID\\ResponseType\\IdToken' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdToken.php', + 'OAuth2\\OpenID\\ResponseType\\IdTokenInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php', + 'OAuth2\\OpenID\\ResponseType\\IdTokenToken' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenToken.php', + 'OAuth2\\OpenID\\ResponseType\\IdTokenTokenInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php', + 'OAuth2\\OpenID\\Storage\\AuthorizationCodeInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php', + 'OAuth2\\OpenID\\Storage\\UserClaimsInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Storage/UserClaimsInterface.php', + 'OAuth2\\Request' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Request.php', + 'OAuth2\\RequestInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/RequestInterface.php', + 'OAuth2\\Response' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Response.php', + 'OAuth2\\ResponseInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/ResponseInterface.php', + 'OAuth2\\ResponseType\\AccessToken' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AccessToken.php', + 'OAuth2\\ResponseType\\AccessTokenInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AccessTokenInterface.php', + 'OAuth2\\ResponseType\\AuthorizationCode' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCode.php', + 'OAuth2\\ResponseType\\AuthorizationCodeInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCodeInterface.php', + 'OAuth2\\ResponseType\\JwtAccessToken' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/JwtAccessToken.php', + 'OAuth2\\ResponseType\\ResponseTypeInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/ResponseTypeInterface.php', + 'OAuth2\\Scope' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Scope.php', + 'OAuth2\\ScopeInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/ScopeInterface.php', + 'OAuth2\\Server' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Server.php', + 'OAuth2\\Storage\\AccessTokenInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/AccessTokenInterface.php', + 'OAuth2\\Storage\\AuthorizationCodeInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/AuthorizationCodeInterface.php', + 'OAuth2\\Storage\\Cassandra' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/Cassandra.php', + 'OAuth2\\Storage\\ClientCredentialsInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientCredentialsInterface.php', + 'OAuth2\\Storage\\ClientInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientInterface.php', + 'OAuth2\\Storage\\CouchbaseDB' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/CouchbaseDB.php', + 'OAuth2\\Storage\\DynamoDB' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/DynamoDB.php', + 'OAuth2\\Storage\\JwtAccessToken' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessToken.php', + 'OAuth2\\Storage\\JwtAccessTokenInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessTokenInterface.php', + 'OAuth2\\Storage\\JwtBearerInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtBearerInterface.php', + 'OAuth2\\Storage\\Memory' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/Memory.php', + 'OAuth2\\Storage\\Mongo' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/Mongo.php', + 'OAuth2\\Storage\\MongoDB' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/MongoDB.php', + 'OAuth2\\Storage\\Pdo' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/Pdo.php', + 'OAuth2\\Storage\\PublicKeyInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/PublicKeyInterface.php', + 'OAuth2\\Storage\\Redis' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/Redis.php', + 'OAuth2\\Storage\\RefreshTokenInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/RefreshTokenInterface.php', + 'OAuth2\\Storage\\ScopeInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/ScopeInterface.php', + 'OAuth2\\Storage\\UserCredentialsInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/UserCredentialsInterface.php', + 'OAuth2\\TokenType\\Bearer' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Bearer.php', + 'OAuth2\\TokenType\\Mac' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Mac.php', + 'OAuth2\\TokenType\\TokenTypeInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/TokenType/TokenTypeInterface.php', 'Psr\\Log\\AbstractLogger' => $vendorDir . '/psr/log/Psr/Log/AbstractLogger.php', 'Psr\\Log\\InvalidArgumentException' => $vendorDir . '/psr/log/Psr/Log/InvalidArgumentException.php', 'Psr\\Log\\LogLevel' => $vendorDir . '/psr/log/Psr/Log/LogLevel.php', @@ -353,10 +661,46 @@ return array( 'Sabre\\Xml\\Writer' => $vendorDir . '/sabre/xml/lib/Writer.php', 'Sabre\\Xml\\XmlDeserializable' => $vendorDir . '/sabre/xml/lib/XmlDeserializable.php', 'Sabre\\Xml\\XmlSerializable' => $vendorDir . '/sabre/xml/lib/XmlSerializable.php', + 'SimplePie' => $vendorDir . '/simplepie/simplepie/library/SimplePie.php', + 'SimplePie_Author' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Author.php', + 'SimplePie_Cache' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Cache.php', + 'SimplePie_Cache_Base' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Cache/Base.php', + 'SimplePie_Cache_DB' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Cache/DB.php', + 'SimplePie_Cache_File' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Cache/File.php', + 'SimplePie_Cache_Memcache' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Cache/Memcache.php', + 'SimplePie_Cache_Memcached' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Cache/Memcached.php', + 'SimplePie_Cache_MySQL' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Cache/MySQL.php', + 'SimplePie_Cache_Redis' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Cache/Redis.php', + 'SimplePie_Caption' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Caption.php', + 'SimplePie_Category' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Category.php', + 'SimplePie_Content_Type_Sniffer' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Content/Type/Sniffer.php', + 'SimplePie_Copyright' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Copyright.php', + 'SimplePie_Core' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Core.php', + 'SimplePie_Credit' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Credit.php', + 'SimplePie_Decode_HTML_Entities' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Decode/HTML/Entities.php', + 'SimplePie_Enclosure' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Enclosure.php', + 'SimplePie_Exception' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Exception.php', + 'SimplePie_File' => $vendorDir . '/simplepie/simplepie/library/SimplePie/File.php', + 'SimplePie_HTTP_Parser' => $vendorDir . '/simplepie/simplepie/library/SimplePie/HTTP/Parser.php', + 'SimplePie_IRI' => $vendorDir . '/simplepie/simplepie/library/SimplePie/IRI.php', + 'SimplePie_Item' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Item.php', + 'SimplePie_Locator' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Locator.php', + 'SimplePie_Misc' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Misc.php', + 'SimplePie_Net_IPv6' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Net/IPv6.php', + 'SimplePie_Parse_Date' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Parse/Date.php', + 'SimplePie_Parser' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Parser.php', + 'SimplePie_Rating' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Rating.php', + 'SimplePie_Registry' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Registry.php', + 'SimplePie_Restriction' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Restriction.php', + 'SimplePie_Sanitize' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Sanitize.php', + 'SimplePie_Source' => $vendorDir . '/simplepie/simplepie/library/SimplePie/Source.php', + 'SimplePie_XML_Declaration_Parser' => $vendorDir . '/simplepie/simplepie/library/SimplePie/XML/Declaration/Parser.php', + 'SimplePie_gzdecode' => $vendorDir . '/simplepie/simplepie/library/SimplePie/gzdecode.php', 'Zotlabs\\Access\\AccessList' => $baseDir . '/Zotlabs/Access/AccessList.php', 'Zotlabs\\Access\\PermissionLimits' => $baseDir . '/Zotlabs/Access/PermissionLimits.php', 'Zotlabs\\Access\\PermissionRoles' => $baseDir . '/Zotlabs/Access/PermissionRoles.php', 'Zotlabs\\Access\\Permissions' => $baseDir . '/Zotlabs/Access/Permissions.php', + 'Zotlabs\\Daemon\\Addon' => $baseDir . '/Zotlabs/Daemon/Addon.php', 'Zotlabs\\Daemon\\Checksites' => $baseDir . '/Zotlabs/Daemon/Checksites.php', 'Zotlabs\\Daemon\\Cli_suggest' => $baseDir . '/Zotlabs/Daemon/Cli_suggest.php', 'Zotlabs\\Daemon\\Cron' => $baseDir . '/Zotlabs/Daemon/Cron.php', @@ -388,10 +732,15 @@ return array( 'Zotlabs\\Lib\\Cache' => $baseDir . '/Zotlabs/Lib/Cache.php', 'Zotlabs\\Lib\\Chatroom' => $baseDir . '/Zotlabs/Lib/Chatroom.php', 'Zotlabs\\Lib\\Config' => $baseDir . '/Zotlabs/Lib/Config.php', + 'Zotlabs\\Lib\\DB_Upgrade' => $baseDir . '/Zotlabs/Lib/DB_Upgrade.php', 'Zotlabs\\Lib\\Enotify' => $baseDir . '/Zotlabs/Lib/Enotify.php', 'Zotlabs\\Lib\\ExtendedZip' => $baseDir . '/Zotlabs/Lib/ExtendedZip.php', 'Zotlabs\\Lib\\IConfig' => $baseDir . '/Zotlabs/Lib/IConfig.php', + 'Zotlabs\\Lib\\MarkdownSoap' => $baseDir . '/Zotlabs/Lib/MarkdownSoap.php', + 'Zotlabs\\Lib\\NativeWiki' => $baseDir . '/Zotlabs/Lib/NativeWiki.php', + 'Zotlabs\\Lib\\NativeWikiPage' => $baseDir . '/Zotlabs/Lib/NativeWikiPage.php', 'Zotlabs\\Lib\\PConfig' => $baseDir . '/Zotlabs/Lib/PConfig.php', + 'Zotlabs\\Lib\\Permcat' => $baseDir . '/Zotlabs/Lib/Permcat.php', 'Zotlabs\\Lib\\PermissionDescription' => $baseDir . '/Zotlabs/Lib/PermissionDescription.php', 'Zotlabs\\Lib\\ProtoDriver' => $baseDir . '/Zotlabs/Lib/ProtoDriver.php', 'Zotlabs\\Lib\\SuperCurl' => $baseDir . '/Zotlabs/Lib/SuperCurl.php', @@ -478,7 +827,6 @@ return array( 'Zotlabs\\Module\\Magic' => $baseDir . '/Zotlabs/Module/Magic.php', 'Zotlabs\\Module\\Mail' => $baseDir . '/Zotlabs/Module/Mail.php', 'Zotlabs\\Module\\Manage' => $baseDir . '/Zotlabs/Module/Manage.php', - 'Zotlabs\\Module\\Match' => $baseDir . '/Zotlabs/Module/Match.php', 'Zotlabs\\Module\\Menu' => $baseDir . '/Zotlabs/Module/Menu.php', 'Zotlabs\\Module\\Message' => $baseDir . '/Zotlabs/Module/Message.php', 'Zotlabs\\Module\\Mitem' => $baseDir . '/Zotlabs/Module/Mitem.php', @@ -492,10 +840,12 @@ return array( 'Zotlabs\\Module\\Oembed' => $baseDir . '/Zotlabs/Module/Oembed.php', 'Zotlabs\\Module\\Oep' => $baseDir . '/Zotlabs/Module/Oep.php', 'Zotlabs\\Module\\Oexchange' => $baseDir . '/Zotlabs/Module/Oexchange.php', + 'Zotlabs\\Module\\Ofeed' => $baseDir . '/Zotlabs/Module/Ofeed.php', 'Zotlabs\\Module\\Online' => $baseDir . '/Zotlabs/Module/Online.php', 'Zotlabs\\Module\\Page' => $baseDir . '/Zotlabs/Module/Page.php', 'Zotlabs\\Module\\Pconfig' => $baseDir . '/Zotlabs/Module/Pconfig.php', 'Zotlabs\\Module\\Pdledit' => $baseDir . '/Zotlabs/Module/Pdledit.php', + 'Zotlabs\\Module\\Permcat' => $baseDir . '/Zotlabs/Module/Permcat.php', 'Zotlabs\\Module\\Photo' => $baseDir . '/Zotlabs/Module/Photo.php', 'Zotlabs\\Module\\Photos' => $baseDir . '/Zotlabs/Module/Photos.php', 'Zotlabs\\Module\\Ping' => $baseDir . '/Zotlabs/Module/Ping.php', @@ -525,7 +875,6 @@ return array( 'Zotlabs\\Module\\Removeme' => $baseDir . '/Zotlabs/Module/Removeme.php', 'Zotlabs\\Module\\Rmagic' => $baseDir . '/Zotlabs/Module/Rmagic.php', 'Zotlabs\\Module\\Rpost' => $baseDir . '/Zotlabs/Module/Rpost.php', - 'Zotlabs\\Module\\Rsd_xml' => $baseDir . '/Zotlabs/Module/Rsd_xml.php', 'Zotlabs\\Module\\Search' => $baseDir . '/Zotlabs/Module/Search.php', 'Zotlabs\\Module\\Search_ac' => $baseDir . '/Zotlabs/Module/Search_ac.php', 'Zotlabs\\Module\\Service_limits' => $baseDir . '/Zotlabs/Module/Service_limits.php', @@ -536,6 +885,7 @@ return array( 'Zotlabs\\Module\\Settings\\Featured' => $baseDir . '/Zotlabs/Module/Settings/Featured.php', 'Zotlabs\\Module\\Settings\\Features' => $baseDir . '/Zotlabs/Module/Settings/Features.php', 'Zotlabs\\Module\\Settings\\Oauth' => $baseDir . '/Zotlabs/Module/Settings/Oauth.php', + 'Zotlabs\\Module\\Settings\\Permcats' => $baseDir . '/Zotlabs/Module/Settings/Permcats.php', 'Zotlabs\\Module\\Settings\\Tokens' => $baseDir . '/Zotlabs/Module/Settings/Tokens.php', 'Zotlabs\\Module\\Setup' => $baseDir . '/Zotlabs/Module/Setup.php', 'Zotlabs\\Module\\Share' => $baseDir . '/Zotlabs/Module/Share.php', @@ -596,12 +946,65 @@ return array( 'Zotlabs\\Text\\Tagadelic' => $baseDir . '/Zotlabs/Text/Tagadelic.php', 'Zotlabs\\Web\\CheckJS' => $baseDir . '/Zotlabs/Web/CheckJS.php', 'Zotlabs\\Web\\Controller' => $baseDir . '/Zotlabs/Web/Controller.php', + 'Zotlabs\\Web\\HTTPHeaders' => $baseDir . '/Zotlabs/Web/HTTPHeaders.php', 'Zotlabs\\Web\\HttpMeta' => $baseDir . '/Zotlabs/Web/HttpMeta.php', 'Zotlabs\\Web\\Router' => $baseDir . '/Zotlabs/Web/Router.php', 'Zotlabs\\Web\\Session' => $baseDir . '/Zotlabs/Web/Session.php', 'Zotlabs\\Web\\SessionHandler' => $baseDir . '/Zotlabs/Web/SessionHandler.php', 'Zotlabs\\Web\\SubModule' => $baseDir . '/Zotlabs/Web/SubModule.php', 'Zotlabs\\Web\\WebServer' => $baseDir . '/Zotlabs/Web/WebServer.php', + 'Zotlabs\\Widget\\Activity' => $baseDir . '/Zotlabs/Widget/Activity.php', + 'Zotlabs\\Widget\\Admin' => $baseDir . '/Zotlabs/Widget/Admin.php', + 'Zotlabs\\Widget\\Affinity' => $baseDir . '/Zotlabs/Widget/Affinity.php', + 'Zotlabs\\Widget\\Album' => $baseDir . '/Zotlabs/Widget/Album.php', + 'Zotlabs\\Widget\\Appcategories' => $baseDir . '/Zotlabs/Widget/Appcategories.php', + 'Zotlabs\\Widget\\Appcloud' => $baseDir . '/Zotlabs/Widget/Appcloud.php', + 'Zotlabs\\Widget\\Archive' => $baseDir . '/Zotlabs/Widget/Archive.php', + 'Zotlabs\\Widget\\Bookmarkedchats' => $baseDir . '/Zotlabs/Widget/Bookmarkedchats.php', + 'Zotlabs\\Widget\\Catcloud_wall' => $baseDir . '/Zotlabs/Widget/Catcloud_wall.php', + 'Zotlabs\\Widget\\Categories' => $baseDir . '/Zotlabs/Widget/Categories.php', + 'Zotlabs\\Widget\\Chatroom_list' => $baseDir . '/Zotlabs/Widget/Chatroom_list.php', + 'Zotlabs\\Widget\\Chatroom_members' => $baseDir . '/Zotlabs/Widget/Chatroom_members.php', + 'Zotlabs\\Widget\\Clock' => $baseDir . '/Zotlabs/Widget/Clock.php', + 'Zotlabs\\Widget\\Collections' => $baseDir . '/Zotlabs/Widget/Collections.php', + 'Zotlabs\\Widget\\Conversations' => $baseDir . '/Zotlabs/Widget/Conversations.php', + 'Zotlabs\\Widget\\Cover_photo' => $baseDir . '/Zotlabs/Widget/Cover_photo.php', + 'Zotlabs\\Widget\\Design_tools' => $baseDir . '/Zotlabs/Widget/Design_tools.php', + 'Zotlabs\\Widget\\Dirsort' => $baseDir . '/Zotlabs/Widget/Dirsort.php', + 'Zotlabs\\Widget\\Dirtags' => $baseDir . '/Zotlabs/Widget/Dirtags.php', + 'Zotlabs\\Widget\\Eventstools' => $baseDir . '/Zotlabs/Widget/Eventstools.php', + 'Zotlabs\\Widget\\Filer' => $baseDir . '/Zotlabs/Widget/Filer.php', + 'Zotlabs\\Widget\\Findpeople' => $baseDir . '/Zotlabs/Widget/Findpeople.php', + 'Zotlabs\\Widget\\Follow' => $baseDir . '/Zotlabs/Widget/Follow.php', + 'Zotlabs\\Widget\\Forums' => $baseDir . '/Zotlabs/Widget/Forums.php', + 'Zotlabs\\Widget\\Fullprofile' => $baseDir . '/Zotlabs/Widget/Fullprofile.php', + 'Zotlabs\\Widget\\Helpindex' => $baseDir . '/Zotlabs/Widget/Helpindex.php', + 'Zotlabs\\Widget\\Item' => $baseDir . '/Zotlabs/Widget/Item.php', + 'Zotlabs\\Widget\\Mailmenu' => $baseDir . '/Zotlabs/Widget/Mailmenu.php', + 'Zotlabs\\Widget\\Menu_preview' => $baseDir . '/Zotlabs/Widget/Menu_preview.php', + 'Zotlabs\\Widget\\Notes' => $baseDir . '/Zotlabs/Widget/Notes.php', + 'Zotlabs\\Widget\\Photo' => $baseDir . '/Zotlabs/Widget/Photo.php', + 'Zotlabs\\Widget\\Photo_albums' => $baseDir . '/Zotlabs/Widget/Photo_albums.php', + 'Zotlabs\\Widget\\Photo_rand' => $baseDir . '/Zotlabs/Widget/Photo_rand.php', + 'Zotlabs\\Widget\\Profile' => $baseDir . '/Zotlabs/Widget/Profile.php', + 'Zotlabs\\Widget\\Pubsites' => $baseDir . '/Zotlabs/Widget/Pubsites.php', + 'Zotlabs\\Widget\\Random_block' => $baseDir . '/Zotlabs/Widget/Random_block.php', + 'Zotlabs\\Widget\\Rating' => $baseDir . '/Zotlabs/Widget/Rating.php', + 'Zotlabs\\Widget\\Savedsearch' => $baseDir . '/Zotlabs/Widget/Savedsearch.php', + 'Zotlabs\\Widget\\Settings_menu' => $baseDir . '/Zotlabs/Widget/Settings_menu.php', + 'Zotlabs\\Widget\\Shortprofile' => $baseDir . '/Zotlabs/Widget/Shortprofile.php', + 'Zotlabs\\Widget\\Sitesearch' => $baseDir . '/Zotlabs/Widget/Sitesearch.php', + 'Zotlabs\\Widget\\Suggestedchats' => $baseDir . '/Zotlabs/Widget/Suggestedchats.php', + 'Zotlabs\\Widget\\Suggestions' => $baseDir . '/Zotlabs/Widget/Suggestions.php', + 'Zotlabs\\Widget\\Tagcloud' => $baseDir . '/Zotlabs/Widget/Tagcloud.php', + 'Zotlabs\\Widget\\Tagcloud_wall' => $baseDir . '/Zotlabs/Widget/Tagcloud_wall.php', + 'Zotlabs\\Widget\\Tasklist' => $baseDir . '/Zotlabs/Widget/Tasklist.php', + 'Zotlabs\\Widget\\Vcard' => $baseDir . '/Zotlabs/Widget/Vcard.php', + 'Zotlabs\\Widget\\Website_portation_tools' => $baseDir . '/Zotlabs/Widget/Website_portation_tools.php', + 'Zotlabs\\Widget\\Wiki_list' => $baseDir . '/Zotlabs/Widget/Wiki_list.php', + 'Zotlabs\\Widget\\Wiki_page_history' => $baseDir . '/Zotlabs/Widget/Wiki_page_history.php', + 'Zotlabs\\Widget\\Wiki_pages' => $baseDir . '/Zotlabs/Widget/Wiki_pages.php', + 'Zotlabs\\Widget\\Zcard' => $baseDir . '/Zotlabs/Widget/Zcard.php', 'Zotlabs\\Zot\\Auth' => $baseDir . '/Zotlabs/Zot/Auth.php', 'Zotlabs\\Zot\\DReport' => $baseDir . '/Zotlabs/Zot/DReport.php', 'Zotlabs\\Zot\\Finger' => $baseDir . '/Zotlabs/Zot/Finger.php', diff --git a/vendor/composer/autoload_files.php b/vendor/composer/autoload_files.php index a78cbe6fb..bbe6fd553 100644 --- a/vendor/composer/autoload_files.php +++ b/vendor/composer/autoload_files.php @@ -13,4 +13,5 @@ return array( '3569eecfeed3bcf0bad3c998a494ecb8' => $vendorDir . '/sabre/xml/lib/Deserializer/functions.php', '93aa591bc4ca510c520999e34229ee79' => $vendorDir . '/sabre/xml/lib/Serializer/functions.php', 'ebdb698ed4152ae445614b69b5e4bb6a' => $vendorDir . '/sabre/http/lib/functions.php', + '2cffec82183ee1cea088009cef9a6fc3' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php', ); diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php index b7fc0125d..d7070bbbf 100644 --- a/vendor/composer/autoload_namespaces.php +++ b/vendor/composer/autoload_namespaces.php @@ -6,4 +6,8 @@ $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( + 'SimplePie' => array($vendorDir . '/simplepie/simplepie/library'), + 'OAuth2' => array($vendorDir . '/bshaffer/oauth2-server-php/src'), + 'Michelf' => array($vendorDir . '/michelf/php-markdown'), + 'HTMLPurifier' => array($vendorDir . '/ezyang/htmlpurifier/library'), ); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index e8ea2ed78..00a183cc1 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -7,6 +7,7 @@ $baseDir = dirname($vendorDir); return array( 'Zotlabs\\' => array($baseDir . '/Zotlabs'), + 'Test\\Markdownify\\' => array($vendorDir . '/pixel418/markdownify/test'), 'Sabre\\Xml\\' => array($vendorDir . '/sabre/xml/lib'), 'Sabre\\VObject\\' => array($vendorDir . '/sabre/vobject/lib'), 'Sabre\\Uri\\' => array($vendorDir . '/sabre/uri/lib'), @@ -17,5 +18,6 @@ return array( 'Sabre\\CardDAV\\' => array($vendorDir . '/sabre/dav/lib/CardDAV'), 'Sabre\\CalDAV\\' => array($vendorDir . '/sabre/dav/lib/CalDAV'), 'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'), + 'Markdownify\\' => array($vendorDir . '/pixel418/markdownify/src'), 'Hubzilla\\' => array($baseDir . '/include'), ); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php index 24b45085d..bcae78e29 100644 --- a/vendor/composer/autoload_real.php +++ b/vendor/composer/autoload_real.php @@ -23,7 +23,7 @@ class ComposerAutoloaderInit7b34d7e50a62201ec5d5e526a5b8b35d self::$loader = $loader = new \Composer\Autoload\ClassLoader(); spl_autoload_unregister(array('ComposerAutoloaderInit7b34d7e50a62201ec5d5e526a5b8b35d', 'loadClassLoader')); - $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION'); + $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { require_once __DIR__ . '/autoload_static.php'; diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 7f2551665..53b9b20c3 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -14,6 +14,7 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d '3569eecfeed3bcf0bad3c998a494ecb8' => __DIR__ . '/..' . '/sabre/xml/lib/Deserializer/functions.php', '93aa591bc4ca510c520999e34229ee79' => __DIR__ . '/..' . '/sabre/xml/lib/Serializer/functions.php', 'ebdb698ed4152ae445614b69b5e4bb6a' => __DIR__ . '/..' . '/sabre/http/lib/functions.php', + '2cffec82183ee1cea088009cef9a6fc3' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php', ); public static $prefixLengthsPsr4 = array ( @@ -21,6 +22,10 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d array ( 'Zotlabs\\' => 8, ), + 'T' => + array ( + 'Test\\Markdownify\\' => 17, + ), 'S' => array ( 'Sabre\\Xml\\' => 10, @@ -37,6 +42,10 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d array ( 'Psr\\Log\\' => 8, ), + 'M' => + array ( + 'Markdownify\\' => 12, + ), 'H' => array ( 'Hubzilla\\' => 9, @@ -48,6 +57,10 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d array ( 0 => __DIR__ . '/../..' . '/Zotlabs', ), + 'Test\\Markdownify\\' => + array ( + 0 => __DIR__ . '/..' . '/pixel418/markdownify/test', + ), 'Sabre\\Xml\\' => array ( 0 => __DIR__ . '/..' . '/sabre/xml/lib', @@ -88,14 +101,357 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d array ( 0 => __DIR__ . '/..' . '/psr/log/Psr/Log', ), + 'Markdownify\\' => + array ( + 0 => __DIR__ . '/..' . '/pixel418/markdownify/src', + ), 'Hubzilla\\' => array ( 0 => __DIR__ . '/../..' . '/include', ), ); + public static $prefixesPsr0 = array ( + 'S' => + array ( + 'SimplePie' => + array ( + 0 => __DIR__ . '/..' . '/simplepie/simplepie/library', + ), + ), + 'O' => + array ( + 'OAuth2' => + array ( + 0 => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src', + ), + ), + 'M' => + array ( + 'Michelf' => + array ( + 0 => __DIR__ . '/..' . '/michelf/php-markdown', + ), + ), + 'H' => + array ( + 'HTMLPurifier' => + array ( + 0 => __DIR__ . '/..' . '/ezyang/htmlpurifier/library', + ), + ), + ); + public static $classMap = array ( + 'HTMLPurifier' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier.php', + 'HTMLPurifier_Arborize' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Arborize.php', + 'HTMLPurifier_AttrCollections' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrCollections.php', + 'HTMLPurifier_AttrDef' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef.php', + 'HTMLPurifier_AttrDef_CSS' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS.php', + 'HTMLPurifier_AttrDef_CSS_AlphaValue' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/AlphaValue.php', + 'HTMLPurifier_AttrDef_CSS_Background' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Background.php', + 'HTMLPurifier_AttrDef_CSS_BackgroundPosition' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php', + 'HTMLPurifier_AttrDef_CSS_Border' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Border.php', + 'HTMLPurifier_AttrDef_CSS_Color' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Color.php', + 'HTMLPurifier_AttrDef_CSS_Composite' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Composite.php', + 'HTMLPurifier_AttrDef_CSS_DenyElementDecorator' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php', + 'HTMLPurifier_AttrDef_CSS_Filter' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Filter.php', + 'HTMLPurifier_AttrDef_CSS_Font' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Font.php', + 'HTMLPurifier_AttrDef_CSS_FontFamily' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/FontFamily.php', + 'HTMLPurifier_AttrDef_CSS_Ident' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ident.php', + 'HTMLPurifier_AttrDef_CSS_ImportantDecorator' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/ImportantDecorator.php', + 'HTMLPurifier_AttrDef_CSS_Length' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Length.php', + 'HTMLPurifier_AttrDef_CSS_ListStyle' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/ListStyle.php', + 'HTMLPurifier_AttrDef_CSS_Multiple' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Multiple.php', + 'HTMLPurifier_AttrDef_CSS_Number' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Number.php', + 'HTMLPurifier_AttrDef_CSS_Percentage' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Percentage.php', + 'HTMLPurifier_AttrDef_CSS_TextDecoration' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/TextDecoration.php', + 'HTMLPurifier_AttrDef_CSS_URI' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/URI.php', + 'HTMLPurifier_AttrDef_Clone' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Clone.php', + 'HTMLPurifier_AttrDef_Enum' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Enum.php', + 'HTMLPurifier_AttrDef_HTML_Bool' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Bool.php', + 'HTMLPurifier_AttrDef_HTML_Class' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Class.php', + 'HTMLPurifier_AttrDef_HTML_Color' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Color.php', + 'HTMLPurifier_AttrDef_HTML_FrameTarget' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/FrameTarget.php', + 'HTMLPurifier_AttrDef_HTML_ID' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/ID.php', + 'HTMLPurifier_AttrDef_HTML_Length' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Length.php', + 'HTMLPurifier_AttrDef_HTML_LinkTypes' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/LinkTypes.php', + 'HTMLPurifier_AttrDef_HTML_MultiLength' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/MultiLength.php', + 'HTMLPurifier_AttrDef_HTML_Nmtokens' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Nmtokens.php', + 'HTMLPurifier_AttrDef_HTML_Pixels' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Pixels.php', + 'HTMLPurifier_AttrDef_Integer' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Integer.php', + 'HTMLPurifier_AttrDef_Lang' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Lang.php', + 'HTMLPurifier_AttrDef_Switch' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Switch.php', + 'HTMLPurifier_AttrDef_Text' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Text.php', + 'HTMLPurifier_AttrDef_URI' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI.php', + 'HTMLPurifier_AttrDef_URI_Email' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Email.php', + 'HTMLPurifier_AttrDef_URI_Email_SimpleCheck' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Email/SimpleCheck.php', + 'HTMLPurifier_AttrDef_URI_Host' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php', + 'HTMLPurifier_AttrDef_URI_IPv4' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv4.php', + 'HTMLPurifier_AttrDef_URI_IPv6' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv6.php', + 'HTMLPurifier_AttrTransform' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform.php', + 'HTMLPurifier_AttrTransform_Background' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Background.php', + 'HTMLPurifier_AttrTransform_BdoDir' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BdoDir.php', + 'HTMLPurifier_AttrTransform_BgColor' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BgColor.php', + 'HTMLPurifier_AttrTransform_BoolToCSS' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BoolToCSS.php', + 'HTMLPurifier_AttrTransform_Border' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Border.php', + 'HTMLPurifier_AttrTransform_EnumToCSS' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/EnumToCSS.php', + 'HTMLPurifier_AttrTransform_ImgRequired' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgRequired.php', + 'HTMLPurifier_AttrTransform_ImgSpace' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgSpace.php', + 'HTMLPurifier_AttrTransform_Input' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Input.php', + 'HTMLPurifier_AttrTransform_Lang' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Lang.php', + 'HTMLPurifier_AttrTransform_Length' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Length.php', + 'HTMLPurifier_AttrTransform_Name' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Name.php', + 'HTMLPurifier_AttrTransform_NameSync' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/NameSync.php', + 'HTMLPurifier_AttrTransform_Nofollow' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Nofollow.php', + 'HTMLPurifier_AttrTransform_SafeEmbed' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeEmbed.php', + 'HTMLPurifier_AttrTransform_SafeObject' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeObject.php', + 'HTMLPurifier_AttrTransform_SafeParam' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeParam.php', + 'HTMLPurifier_AttrTransform_ScriptRequired' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ScriptRequired.php', + 'HTMLPurifier_AttrTransform_TargetBlank' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetBlank.php', + 'HTMLPurifier_AttrTransform_TargetNoopener' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetNoopener.php', + 'HTMLPurifier_AttrTransform_TargetNoreferrer' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetNoreferrer.php', + 'HTMLPurifier_AttrTransform_Textarea' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Textarea.php', + 'HTMLPurifier_AttrTypes' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrTypes.php', + 'HTMLPurifier_AttrValidator' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/AttrValidator.php', + 'HTMLPurifier_Bootstrap' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Bootstrap.php', + 'HTMLPurifier_CSSDefinition' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/CSSDefinition.php', + 'HTMLPurifier_ChildDef' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef.php', + 'HTMLPurifier_ChildDef_Chameleon' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Chameleon.php', + 'HTMLPurifier_ChildDef_Custom' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Custom.php', + 'HTMLPurifier_ChildDef_Empty' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Empty.php', + 'HTMLPurifier_ChildDef_List' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/List.php', + 'HTMLPurifier_ChildDef_Optional' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Optional.php', + 'HTMLPurifier_ChildDef_Required' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Required.php', + 'HTMLPurifier_ChildDef_StrictBlockquote' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/StrictBlockquote.php', + 'HTMLPurifier_ChildDef_Table' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Table.php', + 'HTMLPurifier_Config' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Config.php', + 'HTMLPurifier_ConfigSchema' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema.php', + 'HTMLPurifier_ConfigSchema_Builder_ConfigSchema' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php', + 'HTMLPurifier_ConfigSchema_Builder_Xml' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/Xml.php', + 'HTMLPurifier_ConfigSchema_Exception' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Exception.php', + 'HTMLPurifier_ConfigSchema_Interchange' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange.php', + 'HTMLPurifier_ConfigSchema_InterchangeBuilder' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/InterchangeBuilder.php', + 'HTMLPurifier_ConfigSchema_Interchange_Directive' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Directive.php', + 'HTMLPurifier_ConfigSchema_Interchange_Id' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Id.php', + 'HTMLPurifier_ConfigSchema_Validator' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Validator.php', + 'HTMLPurifier_ConfigSchema_ValidatorAtom' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/ValidatorAtom.php', + 'HTMLPurifier_ContentSets' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ContentSets.php', + 'HTMLPurifier_Context' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Context.php', + 'HTMLPurifier_Definition' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Definition.php', + 'HTMLPurifier_DefinitionCache' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache.php', + 'HTMLPurifier_DefinitionCacheFactory' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCacheFactory.php', + 'HTMLPurifier_DefinitionCache_Decorator' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator.php', + 'HTMLPurifier_DefinitionCache_Decorator_Cleanup' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php', + 'HTMLPurifier_DefinitionCache_Decorator_Memory' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Memory.php', + 'HTMLPurifier_DefinitionCache_Null' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Null.php', + 'HTMLPurifier_DefinitionCache_Serializer' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer.php', + 'HTMLPurifier_Doctype' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Doctype.php', + 'HTMLPurifier_DoctypeRegistry' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/DoctypeRegistry.php', + 'HTMLPurifier_ElementDef' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ElementDef.php', + 'HTMLPurifier_Encoder' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Encoder.php', + 'HTMLPurifier_EntityLookup' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup.php', + 'HTMLPurifier_EntityParser' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/EntityParser.php', + 'HTMLPurifier_ErrorCollector' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ErrorCollector.php', + 'HTMLPurifier_ErrorStruct' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/ErrorStruct.php', + 'HTMLPurifier_Exception' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Exception.php', + 'HTMLPurifier_Filter' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Filter.php', + 'HTMLPurifier_Filter_ExtractStyleBlocks' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Filter/ExtractStyleBlocks.php', + 'HTMLPurifier_Filter_YouTube' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Filter/YouTube.php', + 'HTMLPurifier_Generator' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Generator.php', + 'HTMLPurifier_HTMLDefinition' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLDefinition.php', + 'HTMLPurifier_HTMLModule' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule.php', + 'HTMLPurifier_HTMLModuleManager' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModuleManager.php', + 'HTMLPurifier_HTMLModule_Bdo' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Bdo.php', + 'HTMLPurifier_HTMLModule_CommonAttributes' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/CommonAttributes.php', + 'HTMLPurifier_HTMLModule_Edit' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Edit.php', + 'HTMLPurifier_HTMLModule_Forms' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Forms.php', + 'HTMLPurifier_HTMLModule_Hypertext' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Hypertext.php', + 'HTMLPurifier_HTMLModule_Iframe' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Iframe.php', + 'HTMLPurifier_HTMLModule_Image' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Image.php', + 'HTMLPurifier_HTMLModule_Legacy' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Legacy.php', + 'HTMLPurifier_HTMLModule_List' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/List.php', + 'HTMLPurifier_HTMLModule_Name' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Name.php', + 'HTMLPurifier_HTMLModule_Nofollow' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Nofollow.php', + 'HTMLPurifier_HTMLModule_NonXMLCommonAttributes' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php', + 'HTMLPurifier_HTMLModule_Object' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Object.php', + 'HTMLPurifier_HTMLModule_Presentation' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Presentation.php', + 'HTMLPurifier_HTMLModule_Proprietary' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Proprietary.php', + 'HTMLPurifier_HTMLModule_Ruby' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Ruby.php', + 'HTMLPurifier_HTMLModule_SafeEmbed' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeEmbed.php', + 'HTMLPurifier_HTMLModule_SafeObject' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeObject.php', + 'HTMLPurifier_HTMLModule_SafeScripting' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeScripting.php', + 'HTMLPurifier_HTMLModule_Scripting' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Scripting.php', + 'HTMLPurifier_HTMLModule_StyleAttribute' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/StyleAttribute.php', + 'HTMLPurifier_HTMLModule_Tables' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tables.php', + 'HTMLPurifier_HTMLModule_Target' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Target.php', + 'HTMLPurifier_HTMLModule_TargetBlank' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetBlank.php', + 'HTMLPurifier_HTMLModule_TargetNoopener' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoopener.php', + 'HTMLPurifier_HTMLModule_TargetNoreferrer' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoreferrer.php', + 'HTMLPurifier_HTMLModule_Text' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Text.php', + 'HTMLPurifier_HTMLModule_Tidy' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy.php', + 'HTMLPurifier_HTMLModule_Tidy_Name' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Name.php', + 'HTMLPurifier_HTMLModule_Tidy_Proprietary' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Proprietary.php', + 'HTMLPurifier_HTMLModule_Tidy_Strict' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Strict.php', + 'HTMLPurifier_HTMLModule_Tidy_Transitional' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Transitional.php', + 'HTMLPurifier_HTMLModule_Tidy_XHTML' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/XHTML.php', + 'HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php', + 'HTMLPurifier_HTMLModule_XMLCommonAttributes' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/XMLCommonAttributes.php', + 'HTMLPurifier_IDAccumulator' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/IDAccumulator.php', + 'HTMLPurifier_Injector' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Injector.php', + 'HTMLPurifier_Injector_AutoParagraph' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Injector/AutoParagraph.php', + 'HTMLPurifier_Injector_DisplayLinkURI' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Injector/DisplayLinkURI.php', + 'HTMLPurifier_Injector_Linkify' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Injector/Linkify.php', + 'HTMLPurifier_Injector_PurifierLinkify' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Injector/PurifierLinkify.php', + 'HTMLPurifier_Injector_RemoveEmpty' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Injector/RemoveEmpty.php', + 'HTMLPurifier_Injector_RemoveSpansWithoutAttributes' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Injector/RemoveSpansWithoutAttributes.php', + 'HTMLPurifier_Injector_SafeObject' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Injector/SafeObject.php', + 'HTMLPurifier_Language' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Language.php', + 'HTMLPurifier_LanguageFactory' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/LanguageFactory.php', + 'HTMLPurifier_Language_en_x_test' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Language/classes/en-x-test.php', + 'HTMLPurifier_Length' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Length.php', + 'HTMLPurifier_Lexer' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Lexer.php', + 'HTMLPurifier_Lexer_DOMLex' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DOMLex.php', + 'HTMLPurifier_Lexer_DirectLex' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DirectLex.php', + 'HTMLPurifier_Lexer_PH5P' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/PH5P.php', + 'HTMLPurifier_Node' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Node.php', + 'HTMLPurifier_Node_Comment' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Node/Comment.php', + 'HTMLPurifier_Node_Element' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Node/Element.php', + 'HTMLPurifier_Node_Text' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Node/Text.php', + 'HTMLPurifier_PercentEncoder' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/PercentEncoder.php', + 'HTMLPurifier_Printer' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Printer.php', + 'HTMLPurifier_Printer_CSSDefinition' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Printer/CSSDefinition.php', + 'HTMLPurifier_Printer_ConfigForm' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.php', + 'HTMLPurifier_Printer_ConfigForm_NullDecorator' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.php', + 'HTMLPurifier_Printer_ConfigForm_bool' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.php', + 'HTMLPurifier_Printer_ConfigForm_default' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.php', + 'HTMLPurifier_Printer_HTMLDefinition' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Printer/HTMLDefinition.php', + 'HTMLPurifier_PropertyList' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/PropertyList.php', + 'HTMLPurifier_PropertyListIterator' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/PropertyListIterator.php', + 'HTMLPurifier_Queue' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Queue.php', + 'HTMLPurifier_Strategy' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Strategy.php', + 'HTMLPurifier_Strategy_Composite' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/Composite.php', + 'HTMLPurifier_Strategy_Core' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/Core.php', + 'HTMLPurifier_Strategy_FixNesting' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/FixNesting.php', + 'HTMLPurifier_Strategy_MakeWellFormed' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/MakeWellFormed.php', + 'HTMLPurifier_Strategy_RemoveForeignElements' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/RemoveForeignElements.php', + 'HTMLPurifier_Strategy_ValidateAttributes' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/ValidateAttributes.php', + 'HTMLPurifier_StringHash' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/StringHash.php', + 'HTMLPurifier_StringHashParser' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/StringHashParser.php', + 'HTMLPurifier_TagTransform' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/TagTransform.php', + 'HTMLPurifier_TagTransform_Font' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/TagTransform/Font.php', + 'HTMLPurifier_TagTransform_Simple' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/TagTransform/Simple.php', + 'HTMLPurifier_Token' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Token.php', + 'HTMLPurifier_TokenFactory' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/TokenFactory.php', + 'HTMLPurifier_Token_Comment' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Token/Comment.php', + 'HTMLPurifier_Token_Empty' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Token/Empty.php', + 'HTMLPurifier_Token_End' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Token/End.php', + 'HTMLPurifier_Token_Start' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Token/Start.php', + 'HTMLPurifier_Token_Tag' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Token/Tag.php', + 'HTMLPurifier_Token_Text' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Token/Text.php', + 'HTMLPurifier_URI' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URI.php', + 'HTMLPurifier_URIDefinition' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIDefinition.php', + 'HTMLPurifier_URIFilter' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter.php', + 'HTMLPurifier_URIFilter_DisableExternal' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/DisableExternal.php', + 'HTMLPurifier_URIFilter_DisableExternalResources' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/DisableExternalResources.php', + 'HTMLPurifier_URIFilter_DisableResources' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/DisableResources.php', + 'HTMLPurifier_URIFilter_HostBlacklist' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/HostBlacklist.php', + 'HTMLPurifier_URIFilter_MakeAbsolute' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/MakeAbsolute.php', + 'HTMLPurifier_URIFilter_Munge' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/Munge.php', + 'HTMLPurifier_URIFilter_SafeIframe' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/SafeIframe.php', + 'HTMLPurifier_URIParser' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIParser.php', + 'HTMLPurifier_URIScheme' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme.php', + 'HTMLPurifier_URISchemeRegistry' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URISchemeRegistry.php', + 'HTMLPurifier_URIScheme_data' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/data.php', + 'HTMLPurifier_URIScheme_file' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/file.php', + 'HTMLPurifier_URIScheme_ftp' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/ftp.php', + 'HTMLPurifier_URIScheme_http' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/http.php', + 'HTMLPurifier_URIScheme_https' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/https.php', + 'HTMLPurifier_URIScheme_mailto' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/mailto.php', + 'HTMLPurifier_URIScheme_news' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/news.php', + 'HTMLPurifier_URIScheme_nntp' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/nntp.php', + 'HTMLPurifier_URIScheme_tel' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/tel.php', + 'HTMLPurifier_UnitConverter' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/UnitConverter.php', + 'HTMLPurifier_VarParser' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/VarParser.php', + 'HTMLPurifier_VarParserException' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/VarParserException.php', + 'HTMLPurifier_VarParser_Flexible' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/VarParser/Flexible.php', + 'HTMLPurifier_VarParser_Native' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/VarParser/Native.php', + 'HTMLPurifier_Zipper' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier/Zipper.php', 'Hubzilla\\Import\\Import' => __DIR__ . '/../..' . '/include/Import/Importer.php', + 'Markdownify\\Converter' => __DIR__ . '/..' . '/pixel418/markdownify/src/Converter.php', + 'Markdownify\\ConverterExtra' => __DIR__ . '/..' . '/pixel418/markdownify/src/ConverterExtra.php', + 'Markdownify\\Parser' => __DIR__ . '/..' . '/pixel418/markdownify/src/Parser.php', + 'Michelf\\Markdown' => __DIR__ . '/..' . '/michelf/php-markdown/Michelf/Markdown.php', + 'Michelf\\MarkdownExtra' => __DIR__ . '/..' . '/michelf/php-markdown/Michelf/MarkdownExtra.php', + 'Michelf\\MarkdownInterface' => __DIR__ . '/..' . '/michelf/php-markdown/Michelf/MarkdownInterface.php', + 'OAuth2\\Autoloader' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Autoloader.php', + 'OAuth2\\ClientAssertionType\\ClientAssertionTypeInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php', + 'OAuth2\\ClientAssertionType\\HttpBasic' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/HttpBasic.php', + 'OAuth2\\Controller\\AuthorizeController' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeController.php', + 'OAuth2\\Controller\\AuthorizeControllerInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeControllerInterface.php', + 'OAuth2\\Controller\\ResourceController' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceController.php', + 'OAuth2\\Controller\\ResourceControllerInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceControllerInterface.php', + 'OAuth2\\Controller\\TokenController' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenController.php', + 'OAuth2\\Controller\\TokenControllerInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenControllerInterface.php', + 'OAuth2\\Encryption\\EncryptionInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Encryption/EncryptionInterface.php', + 'OAuth2\\Encryption\\FirebaseJwt' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Encryption/FirebaseJwt.php', + 'OAuth2\\Encryption\\Jwt' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Encryption/Jwt.php', + 'OAuth2\\GrantType\\AuthorizationCode' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/GrantType/AuthorizationCode.php', + 'OAuth2\\GrantType\\ClientCredentials' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/GrantType/ClientCredentials.php', + 'OAuth2\\GrantType\\GrantTypeInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/GrantType/GrantTypeInterface.php', + 'OAuth2\\GrantType\\JwtBearer' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/GrantType/JwtBearer.php', + 'OAuth2\\GrantType\\RefreshToken' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/GrantType/RefreshToken.php', + 'OAuth2\\GrantType\\UserCredentials' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/GrantType/UserCredentials.php', + 'OAuth2\\OpenID\\Controller\\AuthorizeController' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeController.php', + 'OAuth2\\OpenID\\Controller\\AuthorizeControllerInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php', + 'OAuth2\\OpenID\\Controller\\UserInfoController' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/UserInfoController.php', + 'OAuth2\\OpenID\\Controller\\UserInfoControllerInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php', + 'OAuth2\\OpenID\\GrantType\\AuthorizationCode' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/GrantType/AuthorizationCode.php', + 'OAuth2\\OpenID\\ResponseType\\AuthorizationCode' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php', + 'OAuth2\\OpenID\\ResponseType\\AuthorizationCodeInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php', + 'OAuth2\\OpenID\\ResponseType\\CodeIdToken' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdToken.php', + 'OAuth2\\OpenID\\ResponseType\\CodeIdTokenInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php', + 'OAuth2\\OpenID\\ResponseType\\IdToken' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdToken.php', + 'OAuth2\\OpenID\\ResponseType\\IdTokenInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php', + 'OAuth2\\OpenID\\ResponseType\\IdTokenToken' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenToken.php', + 'OAuth2\\OpenID\\ResponseType\\IdTokenTokenInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php', + 'OAuth2\\OpenID\\Storage\\AuthorizationCodeInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php', + 'OAuth2\\OpenID\\Storage\\UserClaimsInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Storage/UserClaimsInterface.php', + 'OAuth2\\Request' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Request.php', + 'OAuth2\\RequestInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/RequestInterface.php', + 'OAuth2\\Response' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Response.php', + 'OAuth2\\ResponseInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/ResponseInterface.php', + 'OAuth2\\ResponseType\\AccessToken' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AccessToken.php', + 'OAuth2\\ResponseType\\AccessTokenInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AccessTokenInterface.php', + 'OAuth2\\ResponseType\\AuthorizationCode' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCode.php', + 'OAuth2\\ResponseType\\AuthorizationCodeInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCodeInterface.php', + 'OAuth2\\ResponseType\\JwtAccessToken' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/JwtAccessToken.php', + 'OAuth2\\ResponseType\\ResponseTypeInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/ResponseTypeInterface.php', + 'OAuth2\\Scope' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Scope.php', + 'OAuth2\\ScopeInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/ScopeInterface.php', + 'OAuth2\\Server' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Server.php', + 'OAuth2\\Storage\\AccessTokenInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/AccessTokenInterface.php', + 'OAuth2\\Storage\\AuthorizationCodeInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/AuthorizationCodeInterface.php', + 'OAuth2\\Storage\\Cassandra' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/Cassandra.php', + 'OAuth2\\Storage\\ClientCredentialsInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientCredentialsInterface.php', + 'OAuth2\\Storage\\ClientInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientInterface.php', + 'OAuth2\\Storage\\CouchbaseDB' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/CouchbaseDB.php', + 'OAuth2\\Storage\\DynamoDB' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/DynamoDB.php', + 'OAuth2\\Storage\\JwtAccessToken' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessToken.php', + 'OAuth2\\Storage\\JwtAccessTokenInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessTokenInterface.php', + 'OAuth2\\Storage\\JwtBearerInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtBearerInterface.php', + 'OAuth2\\Storage\\Memory' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/Memory.php', + 'OAuth2\\Storage\\Mongo' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/Mongo.php', + 'OAuth2\\Storage\\MongoDB' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/MongoDB.php', + 'OAuth2\\Storage\\Pdo' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/Pdo.php', + 'OAuth2\\Storage\\PublicKeyInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/PublicKeyInterface.php', + 'OAuth2\\Storage\\Redis' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/Redis.php', + 'OAuth2\\Storage\\RefreshTokenInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/RefreshTokenInterface.php', + 'OAuth2\\Storage\\ScopeInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/ScopeInterface.php', + 'OAuth2\\Storage\\UserCredentialsInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Storage/UserCredentialsInterface.php', + 'OAuth2\\TokenType\\Bearer' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Bearer.php', + 'OAuth2\\TokenType\\Mac' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Mac.php', + 'OAuth2\\TokenType\\TokenTypeInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/TokenType/TokenTypeInterface.php', 'Psr\\Log\\AbstractLogger' => __DIR__ . '/..' . '/psr/log/Psr/Log/AbstractLogger.php', 'Psr\\Log\\InvalidArgumentException' => __DIR__ . '/..' . '/psr/log/Psr/Log/InvalidArgumentException.php', 'Psr\\Log\\LogLevel' => __DIR__ . '/..' . '/psr/log/Psr/Log/LogLevel.php', @@ -442,10 +798,46 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d 'Sabre\\Xml\\Writer' => __DIR__ . '/..' . '/sabre/xml/lib/Writer.php', 'Sabre\\Xml\\XmlDeserializable' => __DIR__ . '/..' . '/sabre/xml/lib/XmlDeserializable.php', 'Sabre\\Xml\\XmlSerializable' => __DIR__ . '/..' . '/sabre/xml/lib/XmlSerializable.php', + 'SimplePie' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie.php', + 'SimplePie_Author' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Author.php', + 'SimplePie_Cache' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Cache.php', + 'SimplePie_Cache_Base' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Cache/Base.php', + 'SimplePie_Cache_DB' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Cache/DB.php', + 'SimplePie_Cache_File' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Cache/File.php', + 'SimplePie_Cache_Memcache' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Cache/Memcache.php', + 'SimplePie_Cache_Memcached' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Cache/Memcached.php', + 'SimplePie_Cache_MySQL' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Cache/MySQL.php', + 'SimplePie_Cache_Redis' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Cache/Redis.php', + 'SimplePie_Caption' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Caption.php', + 'SimplePie_Category' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Category.php', + 'SimplePie_Content_Type_Sniffer' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Content/Type/Sniffer.php', + 'SimplePie_Copyright' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Copyright.php', + 'SimplePie_Core' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Core.php', + 'SimplePie_Credit' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Credit.php', + 'SimplePie_Decode_HTML_Entities' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Decode/HTML/Entities.php', + 'SimplePie_Enclosure' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Enclosure.php', + 'SimplePie_Exception' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Exception.php', + 'SimplePie_File' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/File.php', + 'SimplePie_HTTP_Parser' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/HTTP/Parser.php', + 'SimplePie_IRI' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/IRI.php', + 'SimplePie_Item' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Item.php', + 'SimplePie_Locator' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Locator.php', + 'SimplePie_Misc' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Misc.php', + 'SimplePie_Net_IPv6' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Net/IPv6.php', + 'SimplePie_Parse_Date' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Parse/Date.php', + 'SimplePie_Parser' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Parser.php', + 'SimplePie_Rating' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Rating.php', + 'SimplePie_Registry' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Registry.php', + 'SimplePie_Restriction' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Restriction.php', + 'SimplePie_Sanitize' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Sanitize.php', + 'SimplePie_Source' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/Source.php', + 'SimplePie_XML_Declaration_Parser' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/XML/Declaration/Parser.php', + 'SimplePie_gzdecode' => __DIR__ . '/..' . '/simplepie/simplepie/library/SimplePie/gzdecode.php', 'Zotlabs\\Access\\AccessList' => __DIR__ . '/../..' . '/Zotlabs/Access/AccessList.php', 'Zotlabs\\Access\\PermissionLimits' => __DIR__ . '/../..' . '/Zotlabs/Access/PermissionLimits.php', 'Zotlabs\\Access\\PermissionRoles' => __DIR__ . '/../..' . '/Zotlabs/Access/PermissionRoles.php', 'Zotlabs\\Access\\Permissions' => __DIR__ . '/../..' . '/Zotlabs/Access/Permissions.php', + 'Zotlabs\\Daemon\\Addon' => __DIR__ . '/../..' . '/Zotlabs/Daemon/Addon.php', 'Zotlabs\\Daemon\\Checksites' => __DIR__ . '/../..' . '/Zotlabs/Daemon/Checksites.php', 'Zotlabs\\Daemon\\Cli_suggest' => __DIR__ . '/../..' . '/Zotlabs/Daemon/Cli_suggest.php', 'Zotlabs\\Daemon\\Cron' => __DIR__ . '/../..' . '/Zotlabs/Daemon/Cron.php', @@ -477,10 +869,15 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d 'Zotlabs\\Lib\\Cache' => __DIR__ . '/../..' . '/Zotlabs/Lib/Cache.php', 'Zotlabs\\Lib\\Chatroom' => __DIR__ . '/../..' . '/Zotlabs/Lib/Chatroom.php', 'Zotlabs\\Lib\\Config' => __DIR__ . '/../..' . '/Zotlabs/Lib/Config.php', + 'Zotlabs\\Lib\\DB_Upgrade' => __DIR__ . '/../..' . '/Zotlabs/Lib/DB_Upgrade.php', 'Zotlabs\\Lib\\Enotify' => __DIR__ . '/../..' . '/Zotlabs/Lib/Enotify.php', 'Zotlabs\\Lib\\ExtendedZip' => __DIR__ . '/../..' . '/Zotlabs/Lib/ExtendedZip.php', 'Zotlabs\\Lib\\IConfig' => __DIR__ . '/../..' . '/Zotlabs/Lib/IConfig.php', + 'Zotlabs\\Lib\\MarkdownSoap' => __DIR__ . '/../..' . '/Zotlabs/Lib/MarkdownSoap.php', + 'Zotlabs\\Lib\\NativeWiki' => __DIR__ . '/../..' . '/Zotlabs/Lib/NativeWiki.php', + 'Zotlabs\\Lib\\NativeWikiPage' => __DIR__ . '/../..' . '/Zotlabs/Lib/NativeWikiPage.php', 'Zotlabs\\Lib\\PConfig' => __DIR__ . '/../..' . '/Zotlabs/Lib/PConfig.php', + 'Zotlabs\\Lib\\Permcat' => __DIR__ . '/../..' . '/Zotlabs/Lib/Permcat.php', 'Zotlabs\\Lib\\PermissionDescription' => __DIR__ . '/../..' . '/Zotlabs/Lib/PermissionDescription.php', 'Zotlabs\\Lib\\ProtoDriver' => __DIR__ . '/../..' . '/Zotlabs/Lib/ProtoDriver.php', 'Zotlabs\\Lib\\SuperCurl' => __DIR__ . '/../..' . '/Zotlabs/Lib/SuperCurl.php', @@ -539,7 +936,6 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d 'Zotlabs\\Module\\Events' => __DIR__ . '/../..' . '/Zotlabs/Module/Events.php', 'Zotlabs\\Module\\Fbrowser' => __DIR__ . '/../..' . '/Zotlabs/Module/Fbrowser.php', 'Zotlabs\\Module\\Feed' => __DIR__ . '/../..' . '/Zotlabs/Module/Feed.php', - 'Zotlabs\\Module\\Ffsapi' => __DIR__ . '/../..' . '/Zotlabs/Module/Ffsapi.php', 'Zotlabs\\Module\\Fhublocs' => __DIR__ . '/../..' . '/Zotlabs/Module/Fhublocs.php', 'Zotlabs\\Module\\File_upload' => __DIR__ . '/../..' . '/Zotlabs/Module/File_upload.php', 'Zotlabs\\Module\\Filer' => __DIR__ . '/../..' . '/Zotlabs/Module/Filer.php', @@ -568,7 +964,6 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d 'Zotlabs\\Module\\Magic' => __DIR__ . '/../..' . '/Zotlabs/Module/Magic.php', 'Zotlabs\\Module\\Mail' => __DIR__ . '/../..' . '/Zotlabs/Module/Mail.php', 'Zotlabs\\Module\\Manage' => __DIR__ . '/../..' . '/Zotlabs/Module/Manage.php', - 'Zotlabs\\Module\\Match' => __DIR__ . '/../..' . '/Zotlabs/Module/Match.php', 'Zotlabs\\Module\\Menu' => __DIR__ . '/../..' . '/Zotlabs/Module/Menu.php', 'Zotlabs\\Module\\Message' => __DIR__ . '/../..' . '/Zotlabs/Module/Message.php', 'Zotlabs\\Module\\Mitem' => __DIR__ . '/../..' . '/Zotlabs/Module/Mitem.php', @@ -582,11 +977,12 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d 'Zotlabs\\Module\\Oembed' => __DIR__ . '/../..' . '/Zotlabs/Module/Oembed.php', 'Zotlabs\\Module\\Oep' => __DIR__ . '/../..' . '/Zotlabs/Module/Oep.php', 'Zotlabs\\Module\\Oexchange' => __DIR__ . '/../..' . '/Zotlabs/Module/Oexchange.php', + 'Zotlabs\\Module\\Ofeed' => __DIR__ . '/../..' . '/Zotlabs/Module/Ofeed.php', 'Zotlabs\\Module\\Online' => __DIR__ . '/../..' . '/Zotlabs/Module/Online.php', - 'Zotlabs\\Module\\Opensearch' => __DIR__ . '/../..' . '/Zotlabs/Module/Opensearch.php', 'Zotlabs\\Module\\Page' => __DIR__ . '/../..' . '/Zotlabs/Module/Page.php', 'Zotlabs\\Module\\Pconfig' => __DIR__ . '/../..' . '/Zotlabs/Module/Pconfig.php', 'Zotlabs\\Module\\Pdledit' => __DIR__ . '/../..' . '/Zotlabs/Module/Pdledit.php', + 'Zotlabs\\Module\\Permcat' => __DIR__ . '/../..' . '/Zotlabs/Module/Permcat.php', 'Zotlabs\\Module\\Photo' => __DIR__ . '/../..' . '/Zotlabs/Module/Photo.php', 'Zotlabs\\Module\\Photos' => __DIR__ . '/../..' . '/Zotlabs/Module/Photos.php', 'Zotlabs\\Module\\Ping' => __DIR__ . '/../..' . '/Zotlabs/Module/Ping.php', @@ -616,7 +1012,6 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d 'Zotlabs\\Module\\Removeme' => __DIR__ . '/../..' . '/Zotlabs/Module/Removeme.php', 'Zotlabs\\Module\\Rmagic' => __DIR__ . '/../..' . '/Zotlabs/Module/Rmagic.php', 'Zotlabs\\Module\\Rpost' => __DIR__ . '/../..' . '/Zotlabs/Module/Rpost.php', - 'Zotlabs\\Module\\Rsd_xml' => __DIR__ . '/../..' . '/Zotlabs/Module/Rsd_xml.php', 'Zotlabs\\Module\\Search' => __DIR__ . '/../..' . '/Zotlabs/Module/Search.php', 'Zotlabs\\Module\\Search_ac' => __DIR__ . '/../..' . '/Zotlabs/Module/Search_ac.php', 'Zotlabs\\Module\\Service_limits' => __DIR__ . '/../..' . '/Zotlabs/Module/Service_limits.php', @@ -627,6 +1022,7 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d 'Zotlabs\\Module\\Settings\\Featured' => __DIR__ . '/../..' . '/Zotlabs/Module/Settings/Featured.php', 'Zotlabs\\Module\\Settings\\Features' => __DIR__ . '/../..' . '/Zotlabs/Module/Settings/Features.php', 'Zotlabs\\Module\\Settings\\Oauth' => __DIR__ . '/../..' . '/Zotlabs/Module/Settings/Oauth.php', + 'Zotlabs\\Module\\Settings\\Permcats' => __DIR__ . '/../..' . '/Zotlabs/Module/Settings/Permcats.php', 'Zotlabs\\Module\\Settings\\Tokens' => __DIR__ . '/../..' . '/Zotlabs/Module/Settings/Tokens.php', 'Zotlabs\\Module\\Setup' => __DIR__ . '/../..' . '/Zotlabs/Module/Setup.php', 'Zotlabs\\Module\\Share' => __DIR__ . '/../..' . '/Zotlabs/Module/Share.php', @@ -687,12 +1083,65 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d 'Zotlabs\\Text\\Tagadelic' => __DIR__ . '/../..' . '/Zotlabs/Text/Tagadelic.php', 'Zotlabs\\Web\\CheckJS' => __DIR__ . '/../..' . '/Zotlabs/Web/CheckJS.php', 'Zotlabs\\Web\\Controller' => __DIR__ . '/../..' . '/Zotlabs/Web/Controller.php', + 'Zotlabs\\Web\\HTTPHeaders' => __DIR__ . '/../..' . '/Zotlabs/Web/HTTPHeaders.php', 'Zotlabs\\Web\\HttpMeta' => __DIR__ . '/../..' . '/Zotlabs/Web/HttpMeta.php', 'Zotlabs\\Web\\Router' => __DIR__ . '/../..' . '/Zotlabs/Web/Router.php', 'Zotlabs\\Web\\Session' => __DIR__ . '/../..' . '/Zotlabs/Web/Session.php', 'Zotlabs\\Web\\SessionHandler' => __DIR__ . '/../..' . '/Zotlabs/Web/SessionHandler.php', 'Zotlabs\\Web\\SubModule' => __DIR__ . '/../..' . '/Zotlabs/Web/SubModule.php', 'Zotlabs\\Web\\WebServer' => __DIR__ . '/../..' . '/Zotlabs/Web/WebServer.php', + 'Zotlabs\\Widget\\Activity' => __DIR__ . '/../..' . '/Zotlabs/Widget/Activity.php', + 'Zotlabs\\Widget\\Admin' => __DIR__ . '/../..' . '/Zotlabs/Widget/Admin.php', + 'Zotlabs\\Widget\\Affinity' => __DIR__ . '/../..' . '/Zotlabs/Widget/Affinity.php', + 'Zotlabs\\Widget\\Album' => __DIR__ . '/../..' . '/Zotlabs/Widget/Album.php', + 'Zotlabs\\Widget\\Appcategories' => __DIR__ . '/../..' . '/Zotlabs/Widget/Appcategories.php', + 'Zotlabs\\Widget\\Appcloud' => __DIR__ . '/../..' . '/Zotlabs/Widget/Appcloud.php', + 'Zotlabs\\Widget\\Archive' => __DIR__ . '/../..' . '/Zotlabs/Widget/Archive.php', + 'Zotlabs\\Widget\\Bookmarkedchats' => __DIR__ . '/../..' . '/Zotlabs/Widget/Bookmarkedchats.php', + 'Zotlabs\\Widget\\Catcloud_wall' => __DIR__ . '/../..' . '/Zotlabs/Widget/Catcloud_wall.php', + 'Zotlabs\\Widget\\Categories' => __DIR__ . '/../..' . '/Zotlabs/Widget/Categories.php', + 'Zotlabs\\Widget\\Chatroom_list' => __DIR__ . '/../..' . '/Zotlabs/Widget/Chatroom_list.php', + 'Zotlabs\\Widget\\Chatroom_members' => __DIR__ . '/../..' . '/Zotlabs/Widget/Chatroom_members.php', + 'Zotlabs\\Widget\\Clock' => __DIR__ . '/../..' . '/Zotlabs/Widget/Clock.php', + 'Zotlabs\\Widget\\Collections' => __DIR__ . '/../..' . '/Zotlabs/Widget/Collections.php', + 'Zotlabs\\Widget\\Conversations' => __DIR__ . '/../..' . '/Zotlabs/Widget/Conversations.php', + 'Zotlabs\\Widget\\Cover_photo' => __DIR__ . '/../..' . '/Zotlabs/Widget/Cover_photo.php', + 'Zotlabs\\Widget\\Design_tools' => __DIR__ . '/../..' . '/Zotlabs/Widget/Design_tools.php', + 'Zotlabs\\Widget\\Dirsort' => __DIR__ . '/../..' . '/Zotlabs/Widget/Dirsort.php', + 'Zotlabs\\Widget\\Dirtags' => __DIR__ . '/../..' . '/Zotlabs/Widget/Dirtags.php', + 'Zotlabs\\Widget\\Eventstools' => __DIR__ . '/../..' . '/Zotlabs/Widget/Eventstools.php', + 'Zotlabs\\Widget\\Filer' => __DIR__ . '/../..' . '/Zotlabs/Widget/Filer.php', + 'Zotlabs\\Widget\\Findpeople' => __DIR__ . '/../..' . '/Zotlabs/Widget/Findpeople.php', + 'Zotlabs\\Widget\\Follow' => __DIR__ . '/../..' . '/Zotlabs/Widget/Follow.php', + 'Zotlabs\\Widget\\Forums' => __DIR__ . '/../..' . '/Zotlabs/Widget/Forums.php', + 'Zotlabs\\Widget\\Fullprofile' => __DIR__ . '/../..' . '/Zotlabs/Widget/Fullprofile.php', + 'Zotlabs\\Widget\\Helpindex' => __DIR__ . '/../..' . '/Zotlabs/Widget/Helpindex.php', + 'Zotlabs\\Widget\\Item' => __DIR__ . '/../..' . '/Zotlabs/Widget/Item.php', + 'Zotlabs\\Widget\\Mailmenu' => __DIR__ . '/../..' . '/Zotlabs/Widget/Mailmenu.php', + 'Zotlabs\\Widget\\Menu_preview' => __DIR__ . '/../..' . '/Zotlabs/Widget/Menu_preview.php', + 'Zotlabs\\Widget\\Notes' => __DIR__ . '/../..' . '/Zotlabs/Widget/Notes.php', + 'Zotlabs\\Widget\\Photo' => __DIR__ . '/../..' . '/Zotlabs/Widget/Photo.php', + 'Zotlabs\\Widget\\Photo_albums' => __DIR__ . '/../..' . '/Zotlabs/Widget/Photo_albums.php', + 'Zotlabs\\Widget\\Photo_rand' => __DIR__ . '/../..' . '/Zotlabs/Widget/Photo_rand.php', + 'Zotlabs\\Widget\\Profile' => __DIR__ . '/../..' . '/Zotlabs/Widget/Profile.php', + 'Zotlabs\\Widget\\Pubsites' => __DIR__ . '/../..' . '/Zotlabs/Widget/Pubsites.php', + 'Zotlabs\\Widget\\Random_block' => __DIR__ . '/../..' . '/Zotlabs/Widget/Random_block.php', + 'Zotlabs\\Widget\\Rating' => __DIR__ . '/../..' . '/Zotlabs/Widget/Rating.php', + 'Zotlabs\\Widget\\Savedsearch' => __DIR__ . '/../..' . '/Zotlabs/Widget/Savedsearch.php', + 'Zotlabs\\Widget\\Settings_menu' => __DIR__ . '/../..' . '/Zotlabs/Widget/Settings_menu.php', + 'Zotlabs\\Widget\\Shortprofile' => __DIR__ . '/../..' . '/Zotlabs/Widget/Shortprofile.php', + 'Zotlabs\\Widget\\Sitesearch' => __DIR__ . '/../..' . '/Zotlabs/Widget/Sitesearch.php', + 'Zotlabs\\Widget\\Suggestedchats' => __DIR__ . '/../..' . '/Zotlabs/Widget/Suggestedchats.php', + 'Zotlabs\\Widget\\Suggestions' => __DIR__ . '/../..' . '/Zotlabs/Widget/Suggestions.php', + 'Zotlabs\\Widget\\Tagcloud' => __DIR__ . '/../..' . '/Zotlabs/Widget/Tagcloud.php', + 'Zotlabs\\Widget\\Tagcloud_wall' => __DIR__ . '/../..' . '/Zotlabs/Widget/Tagcloud_wall.php', + 'Zotlabs\\Widget\\Tasklist' => __DIR__ . '/../..' . '/Zotlabs/Widget/Tasklist.php', + 'Zotlabs\\Widget\\Vcard' => __DIR__ . '/../..' . '/Zotlabs/Widget/Vcard.php', + 'Zotlabs\\Widget\\Website_portation_tools' => __DIR__ . '/../..' . '/Zotlabs/Widget/Website_portation_tools.php', + 'Zotlabs\\Widget\\Wiki_list' => __DIR__ . '/../..' . '/Zotlabs/Widget/Wiki_list.php', + 'Zotlabs\\Widget\\Wiki_page_history' => __DIR__ . '/../..' . '/Zotlabs/Widget/Wiki_page_history.php', + 'Zotlabs\\Widget\\Wiki_pages' => __DIR__ . '/../..' . '/Zotlabs/Widget/Wiki_pages.php', + 'Zotlabs\\Widget\\Zcard' => __DIR__ . '/../..' . '/Zotlabs/Widget/Zcard.php', 'Zotlabs\\Zot\\Auth' => __DIR__ . '/../..' . '/Zotlabs/Zot/Auth.php', 'Zotlabs\\Zot\\DReport' => __DIR__ . '/../..' . '/Zotlabs/Zot/DReport.php', 'Zotlabs\\Zot\\Finger' => __DIR__ . '/../..' . '/Zotlabs/Zot/Finger.php', @@ -707,6 +1156,7 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d return \Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d::$prefixDirsPsr4; + $loader->prefixesPsr0 = ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d::$prefixesPsr0; $loader->classMap = ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d::$classMap; }, null, ClassLoader::class); diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 88acfc40c..7e4f0bb29 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -21,7 +21,7 @@ "phpunit/phpunit": "*", "sabre/cs": "~0.0.1" }, - "time": "2016-03-08 02:29:27", + "time": "2016-03-08T02:29:27+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -79,7 +79,7 @@ "suggest": { "hoa/bench": "If you would like to run the benchmark scripts" }, - "time": "2016-07-15 19:52:17", + "time": "2016-07-15T19:52:17+00:00", "bin": [ "bin/vobject", "bin/generate_vcards" @@ -173,7 +173,7 @@ "phpunit/phpunit": "*", "sabre/cs": "~0.0.4" }, - "time": "2015-11-05 20:14:39", + "time": "2015-11-05T20:14:39+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -238,7 +238,7 @@ "suggest": { "ext-curl": " to make http requests with the Client class" }, - "time": "2016-01-06 23:00:08", + "time": "2016-01-06T23:00:08+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -310,7 +310,7 @@ "ext-curl": "*", "ext-pdo": "*" }, - "time": "2016-06-28 02:44:05", + "time": "2016-06-28T02:44:05+00:00", "bin": [ "bin/sabredav", "bin/naturalselection" @@ -379,7 +379,7 @@ "phpunit/phpunit": "*", "sabre/cs": "~1.0.0" }, - "time": "2016-10-09 22:57:52", + "time": "2016-10-09T22:57:52+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -435,7 +435,7 @@ "require": { "php": ">=5.3.0" }, - "time": "2016-10-10 12:19:37", + "time": "2016-10-10T12:19:37+00:00", "type": "library", "extra": { "branch-alias": { @@ -465,5 +465,288 @@ "psr", "psr-3" ] + }, + { + "name": "michelf/php-markdown", + "version": "1.7.0", + "version_normalized": "1.7.0.0", + "source": { + "type": "git", + "url": "https://github.com/michelf/php-markdown.git", + "reference": "1f51cc520948f66cd2af8cbc45a5ee175e774220" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/michelf/php-markdown/zipball/1f51cc520948f66cd2af8cbc45a5ee175e774220", + "reference": "1f51cc520948f66cd2af8cbc45a5ee175e774220", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2016-10-29T18:58:20+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-lib": "1.4.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-0": { + "Michelf": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Michel Fortin", + "email": "michel.fortin@michelf.ca", + "homepage": "https://michelf.ca/", + "role": "Developer" + }, + { + "name": "John Gruber", + "homepage": "https://daringfireball.net/" + } + ], + "description": "PHP Markdown", + "homepage": "https://michelf.ca/projects/php-markdown/", + "keywords": [ + "markdown" + ] + }, + { + "name": "pixel418/markdownify", + "version": "v2.2.1", + "version_normalized": "2.2.1.0", + "source": { + "type": "git", + "url": "https://github.com/Elephant418/Markdownify.git", + "reference": "0160677f04c784550dd10fd72fdf3994967db848" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Elephant418/Markdownify/zipball/0160677f04c784550dd10fd72fdf3994967db848", + "reference": "0160677f04c784550dd10fd72fdf3994967db848", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8" + }, + "time": "2016-09-21T13:01:43+00:00", + "type": "lib", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Markdownify\\": "src", + "Test\\Markdownify\\": "test" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL" + ], + "authors": [ + { + "name": "Peter Kruithof", + "email": "pkruithof@gmail.com", + "homepage": "http://pkruithof.tumblr.com/" + }, + { + "name": "Milian Wolff", + "email": "mail@milianw.de", + "homepage": "http://milianw.de" + }, + { + "name": "Thomas Zilliox", + "email": "hello@tzi.fr", + "homepage": "http://tzi.fr" + } + ], + "description": "The HTML to Markdown converter for PHP ", + "homepage": "https://github.com/elephant418/Markdownify", + "keywords": [ + "markdown", + "markdownify" + ] + }, + { + "name": "bshaffer/oauth2-server-php", + "version": "v1.9.0", + "version_normalized": "1.9.0.0", + "source": { + "type": "git", + "url": "https://github.com/bshaffer/oauth2-server-php.git", + "reference": "8856aed1a98d6da596ae3f9b8095b5c7a1581697" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bshaffer/oauth2-server-php/zipball/8856aed1a98d6da596ae3f9b8095b5c7a1581697", + "reference": "8856aed1a98d6da596ae3f9b8095b5c7a1581697", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "aws/aws-sdk-php": "~2.8", + "firebase/php-jwt": "~2.2", + "mongodb/mongodb": "^1.1", + "predis/predis": "dev-master", + "thobbs/phpcassa": "dev-master" + }, + "suggest": { + "aws/aws-sdk-php": "~2.8 is required to use DynamoDB storage", + "firebase/php-jwt": "~1.1 is required to use MondoDB storage", + "predis/predis": "Required to use Redis storage", + "thobbs/phpcassa": "Required to use Cassandra storage" + }, + "time": "2017-01-06T23:20:00+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "OAuth2": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brent Shaffer", + "email": "bshafs@gmail.com", + "homepage": "http://brentertainment.com" + } + ], + "description": "OAuth2 Server for PHP", + "homepage": "http://github.com/bshaffer/oauth2-server-php", + "keywords": [ + "auth", + "oauth", + "oauth2" + ] + }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.9.2", + "version_normalized": "4.9.2.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "6d50e5282afdfdfc3e0ff6d192aff56c5629b3d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/6d50e5282afdfdfc3e0ff6d192aff56c5629b3d4", + "reference": "6d50e5282afdfdfc3e0ff6d192aff56c5629b3d4", + "shasum": "" + }, + "require": { + "php": ">=5.2" + }, + "require-dev": { + "simpletest/simpletest": "^1.1" + }, + "time": "2017-03-13T06:30:53+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "HTMLPurifier": "library/" + }, + "files": [ + "library/HTMLPurifier.composer.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ] + }, + { + "name": "simplepie/simplepie", + "version": "1.5", + "version_normalized": "1.5.0.0", + "source": { + "type": "git", + "url": "https://github.com/simplepie/simplepie.git", + "reference": "5de5551953f95feef12cf355a7a26a70f94aa3ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/simplepie/simplepie/zipball/5de5551953f95feef12cf355a7a26a70f94aa3ab", + "reference": "5de5551953f95feef12cf355a7a26a70f94aa3ab", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "~4 || ~5" + }, + "suggest": { + "mf2/mf2": "Microformat module that allows for parsing HTML for microformats" + }, + "time": "2017-04-17T07:29:31+00:00", + "type": "library", + "installation-source": "source", + "autoload": { + "psr-0": { + "SimplePie": "library" + } + }, + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ryan Parman", + "homepage": "http://ryanparman.com/", + "role": "Creator, alumnus developer" + }, + { + "name": "Geoffrey Sneddon", + "homepage": "http://gsnedders.com/", + "role": "Alumnus developer" + }, + { + "name": "Ryan McCue", + "email": "me@ryanmccue.info", + "homepage": "http://ryanmccue.info/", + "role": "Developer" + } + ], + "description": "A simple Atom/RSS parsing library for PHP", + "homepage": "http://simplepie.org/", + "keywords": [ + "atom", + "feeds", + "rss" + ], + "support": { + "source": "https://github.com/simplepie/simplepie/tree/1.5", + "issues": "https://github.com/simplepie/simplepie/issues" + } } ] diff --git a/vendor/ezyang/htmlpurifier/CREDITS b/vendor/ezyang/htmlpurifier/CREDITS new file mode 100644 index 000000000..7921b45af --- /dev/null +++ b/vendor/ezyang/htmlpurifier/CREDITS @@ -0,0 +1,9 @@ + +CREDITS + +Almost everything written by Edward Z. Yang (Ambush Commander). Lots of thanks +to the DevNetwork Community for their help (see docs/ref-devnetwork.html for +more details), Feyd especially (namely IPv6 and optimization). Thanks to RSnake +for letting me package his fantastic XSS cheatsheet for a smoketest. + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/INSTALL b/vendor/ezyang/htmlpurifier/INSTALL new file mode 100644 index 000000000..e6dd02afa --- /dev/null +++ b/vendor/ezyang/htmlpurifier/INSTALL @@ -0,0 +1,373 @@ + +Install + How to install HTML Purifier + +HTML Purifier is designed to run out of the box, so actually using the +library is extremely easy. (Although... if you were looking for a +step-by-step installation GUI, you've downloaded the wrong software!) + +While the impatient can get going immediately with some of the sample +code at the bottom of this library, it's well worth reading this entire +document--most of the other documentation assumes that you are familiar +with these contents. + + +--------------------------------------------------------------------------- +1. Compatibility + +HTML Purifier is PHP 5 and PHP 7, and is actively tested from PHP 5.0.5 +and up. It has no core dependencies with other libraries. + +These optional extensions can enhance the capabilities of HTML Purifier: + + * iconv : Converts text to and from non-UTF-8 encodings + * bcmath : Used for unit conversion and imagecrash protection + * tidy : Used for pretty-printing HTML + +These optional libraries can enhance the capabilities of HTML Purifier: + + * CSSTidy : Clean CSS stylesheets using %Core.ExtractStyleBlocks + Note: You should use the modernized fork of CSSTidy available + at https://github.com/Cerdic/CSSTidy + * Net_IDNA2 (PEAR) : IRI support using %Core.EnableIDNA + Note: This is not necessary for PHP 5.3 or later + +--------------------------------------------------------------------------- +2. Reconnaissance + +A big plus of HTML Purifier is its inerrant support of standards, so +your web-pages should be standards-compliant. (They should also use +semantic markup, but that's another issue altogether, one HTML Purifier +cannot fix without reading your mind.) + +HTML Purifier can process these doctypes: + +* XHTML 1.0 Transitional (default) +* XHTML 1.0 Strict +* HTML 4.01 Transitional +* HTML 4.01 Strict +* XHTML 1.1 + +...and these character encodings: + +* UTF-8 (default) +* Any encoding iconv supports (with crippled internationalization support) + +These defaults reflect what my choices would be if I were authoring an +HTML document, however, what you choose depends on the nature of your +codebase. If you don't know what doctype you are using, you can determine +the doctype from this identifier at the top of your source code: + + <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + +...and the character encoding from this code: + + <meta http-equiv="Content-type" content="text/html;charset=ENCODING"> + +If the character encoding declaration is missing, STOP NOW, and +read 'docs/enduser-utf8.html' (web accessible at +http://htmlpurifier.org/docs/enduser-utf8.html). In fact, even if it is +present, read this document anyway, as many websites specify their +document's character encoding incorrectly. + + +--------------------------------------------------------------------------- +3. Including the library + +The procedure is quite simple: + + require_once '/path/to/library/HTMLPurifier.auto.php'; + +This will setup an autoloader, so the library's files are only included +when you use them. + +Only the contents in the library/ folder are necessary, so you can remove +everything else when using HTML Purifier in a production environment. + +If you installed HTML Purifier via PEAR, all you need to do is: + + require_once 'HTMLPurifier.auto.php'; + +Please note that the usual PEAR practice of including just the classes you +want will not work with HTML Purifier's autoloading scheme. + +Advanced users, read on; other users can skip to section 4. + +Autoload compatibility +---------------------- + + HTML Purifier attempts to be as smart as possible when registering an + autoloader, but there are some cases where you will need to change + your own code to accomodate HTML Purifier. These are those cases: + + PHP VERSION IS LESS THAN 5.1.2, AND YOU'VE DEFINED __autoload + Because spl_autoload_register() doesn't exist in early versions + of PHP 5, HTML Purifier has no way of adding itself to the autoload + stack. Modify your __autoload function to test + HTMLPurifier_Bootstrap::autoload($class) + + For example, suppose your autoload function looks like this: + + function __autoload($class) { + require str_replace('_', '/', $class) . '.php'; + return true; + } + + A modified version with HTML Purifier would look like this: + + function __autoload($class) { + if (HTMLPurifier_Bootstrap::autoload($class)) return true; + require str_replace('_', '/', $class) . '.php'; + return true; + } + + Note that there *is* some custom behavior in our autoloader; the + original autoloader in our example would work for 99% of the time, + but would fail when including language files. + + AN __autoload FUNCTION IS DECLARED AFTER OUR AUTOLOADER IS REGISTERED + spl_autoload_register() has the curious behavior of disabling + the existing __autoload() handler. Users need to explicitly + spl_autoload_register('__autoload'). Because we use SPL when it + is available, __autoload() will ALWAYS be disabled. If __autoload() + is declared before HTML Purifier is loaded, this is not a problem: + HTML Purifier will register the function for you. But if it is + declared afterwards, it will mysteriously not work. This + snippet of code (after your autoloader is defined) will fix it: + + spl_autoload_register('__autoload') + + Users should also be on guard if they use a version of PHP previous + to 5.1.2 without an autoloader--HTML Purifier will define __autoload() + for you, which can collide with an autoloader that was added by *you* + later. + + +For better performance +---------------------- + + Opcode caches, which greatly speed up PHP initialization for scripts + with large amounts of code (HTML Purifier included), don't like + autoloaders. We offer an include file that includes all of HTML Purifier's + files in one go in an opcode cache friendly manner: + + // If /path/to/library isn't already in your include path, uncomment + // the below line: + // require '/path/to/library/HTMLPurifier.path.php'; + + require 'HTMLPurifier.includes.php'; + + Optional components still need to be included--you'll know if you try to + use a feature and you get a class doesn't exists error! The autoloader + can be used in conjunction with this approach to catch classes that are + missing. Simply add this afterwards: + + require 'HTMLPurifier.autoload.php'; + +Standalone version +------------------ + + HTML Purifier has a standalone distribution; you can also generate + a standalone file from the full version by running the script + maintenance/generate-standalone.php . The standalone version has the + benefit of having most of its code in one file, so parsing is much + faster and the library is easier to manage. + + If HTMLPurifier.standalone.php exists in the library directory, you + can use it like this: + + require '/path/to/HTMLPurifier.standalone.php'; + + This is equivalent to including HTMLPurifier.includes.php, except that + the contents of standalone/ will be added to your path. To override this + behavior, specify a new HTMLPURIFIER_PREFIX where standalone files can + be found (usually, this will be one directory up, the "true" library + directory in full distributions). Don't forget to set your path too! + + The autoloader can be added to the end to ensure the classes are + loaded when necessary; otherwise you can manually include them. + To use the autoloader, use this: + + require 'HTMLPurifier.autoload.php'; + +For advanced users +------------------ + + HTMLPurifier.auto.php performs a number of operations that can be done + individually. These are: + + HTMLPurifier.path.php + Puts /path/to/library in the include path. For high performance, + this should be done in php.ini. + + HTMLPurifier.autoload.php + Registers our autoload handler HTMLPurifier_Bootstrap::autoload($class). + + You can do these operations by yourself--in fact, you must modify your own + autoload handler if you are using a version of PHP earlier than PHP 5.1.2 + (See "Autoload compatibility" above). + + +--------------------------------------------------------------------------- +4. Configuration + +HTML Purifier is designed to run out-of-the-box, but occasionally HTML +Purifier needs to be told what to do. If you answer no to any of these +questions, read on; otherwise, you can skip to the next section (or, if you're +into configuring things just for the heck of it, skip to 4.3). + +* Am I using UTF-8? +* Am I using XHTML 1.0 Transitional? + +If you answered no to any of these questions, instantiate a configuration +object and read on: + + $config = HTMLPurifier_Config::createDefault(); + + +4.1. Setting a different character encoding + +You really shouldn't use any other encoding except UTF-8, especially if you +plan to support multilingual websites (read section three for more details). +However, switching to UTF-8 is not always immediately feasible, so we can +adapt. + +HTML Purifier uses iconv to support other character encodings, as such, +any encoding that iconv supports <http://www.gnu.org/software/libiconv/> +HTML Purifier supports with this code: + + $config->set('Core.Encoding', /* put your encoding here */); + +An example usage for Latin-1 websites (the most common encoding for English +websites): + + $config->set('Core.Encoding', 'ISO-8859-1'); + +Note that HTML Purifier's support for non-Unicode encodings is crippled by the +fact that any character not supported by that encoding will be silently +dropped, EVEN if it is ampersand escaped. If you want to work around +this, you are welcome to read docs/enduser-utf8.html for a fix, +but please be cognizant of the issues the "solution" creates (for this +reason, I do not include the solution in this document). + + +4.2. Setting a different doctype + +For those of you using HTML 4.01 Transitional, you can disable +XHTML output like this: + + $config->set('HTML.Doctype', 'HTML 4.01 Transitional'); + +Other supported doctypes include: + + * HTML 4.01 Strict + * HTML 4.01 Transitional + * XHTML 1.0 Strict + * XHTML 1.0 Transitional + * XHTML 1.1 + + +4.3. Other settings + +There are more configuration directives which can be read about +here: <http://htmlpurifier.org/live/configdoc/plain.html> They're a bit boring, +but they can help out for those of you who like to exert maximum control over +your code. Some of the more interesting ones are configurable at the +demo <http://htmlpurifier.org/demo.php> and are well worth looking into +for your own system. + +For example, you can fine tune allowed elements and attributes, convert +relative URLs to absolute ones, and even autoparagraph input text! These +are, respectively, %HTML.Allowed, %URI.MakeAbsolute and %URI.Base, and +%AutoFormat.AutoParagraph. The %Namespace.Directive naming convention +translates to: + + $config->set('Namespace.Directive', $value); + +E.g. + + $config->set('HTML.Allowed', 'p,b,a[href],i'); + $config->set('URI.Base', 'http://www.example.com'); + $config->set('URI.MakeAbsolute', true); + $config->set('AutoFormat.AutoParagraph', true); + + +--------------------------------------------------------------------------- +5. Caching + +HTML Purifier generates some cache files (generally one or two) to speed up +its execution. For maximum performance, make sure that +library/HTMLPurifier/DefinitionCache/Serializer is writeable by the webserver. + +If you are in the library/ folder of HTML Purifier, you can set the +appropriate permissions using: + + chmod -R 0755 HTMLPurifier/DefinitionCache/Serializer + +If the above command doesn't work, you may need to assign write permissions +to group: + + chmod -R 0775 HTMLPurifier/DefinitionCache/Serializer + +You can also chmod files via your FTP client; this option +is usually accessible by right clicking the corresponding directory and +then selecting "chmod" or "file permissions". + +Starting with 2.0.1, HTML Purifier will generate friendly error messages +that will tell you exactly what you have to chmod the directory to, if in doubt, +follow its advice. + +If you are unable or unwilling to give write permissions to the cache +directory, you can either disable the cache (and suffer a performance +hit): + + $config->set('Core.DefinitionCache', null); + +Or move the cache directory somewhere else (no trailing slash): + + $config->set('Cache.SerializerPath', '/home/user/absolute/path'); + + +--------------------------------------------------------------------------- +6. Using the code + +The interface is mind-numbingly simple: + + $purifier = new HTMLPurifier($config); + $clean_html = $purifier->purify( $dirty_html ); + +That's it! For more examples, check out docs/examples/ (they aren't very +different though). Also, docs/enduser-slow.html gives advice on what to +do if HTML Purifier is slowing down your application. + + +--------------------------------------------------------------------------- +7. Quick install + +First, make sure library/HTMLPurifier/DefinitionCache/Serializer is +writable by the webserver (see Section 5: Caching above for details). +If your website is in UTF-8 and XHTML Transitional, use this code: + +<?php + require_once '/path/to/htmlpurifier/library/HTMLPurifier.auto.php'; + + $config = HTMLPurifier_Config::createDefault(); + $purifier = new HTMLPurifier($config); + $clean_html = $purifier->purify($dirty_html); +?> + +If your website is in a different encoding or doctype, use this code: + +<?php + require_once '/path/to/htmlpurifier/library/HTMLPurifier.auto.php'; + + $config = HTMLPurifier_Config::createDefault(); + $config->set('Core.Encoding', 'ISO-8859-1'); // replace with your encoding + $config->set('HTML.Doctype', 'HTML 4.01 Transitional'); // replace with your doctype + $purifier = new HTMLPurifier($config); + + $clean_html = $purifier->purify($dirty_html); +?> + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/INSTALL.fr.utf8 b/vendor/ezyang/htmlpurifier/INSTALL.fr.utf8 new file mode 100644 index 000000000..95164abba --- /dev/null +++ b/vendor/ezyang/htmlpurifier/INSTALL.fr.utf8 @@ -0,0 +1,60 @@ + +Installation + Comment installer HTML Purifier + +Attention : Ce document est encodé en UTF-8, si les lettres avec des accents +ne s'affichent pas, prenez un meilleur éditeur de texte. + +L'installation de HTML Purifier est très simple, parce qu'il n'a pas besoin +de configuration. Pour les utilisateurs impatients, le code se trouve dans le +pied de page, mais je recommande de lire le document. + +1. Compatibilité + +HTML Purifier fonctionne avec PHP 5. PHP 5.0.5 est la dernière version testée. +Il ne dépend pas d'autres librairies. + +Les extensions optionnelles sont iconv (généralement déjà installée) et tidy +(répendue aussi). Si vous utilisez UTF-8 et que vous ne voulez pas l'indentation, +vous pouvez utiliser HTML Purifier sans ces extensions. + + +2. Inclure la librairie + +Quand vous devez l'utilisez, incluez le : + + require_once('/path/to/library/HTMLPurifier.auto.php'); + +Ne pas l'inclure si ce n'est pas nécessaire, car HTML Purifier est lourd. + +HTML Purifier utilise "autoload". Si vous avez défini la fonction __autoload, +vous devez ajouter cette fonction : + + spl_autoload_register('__autoload') + +Plus d'informations dans le document "INSTALL". + +3. Installation rapide + +Si votre site Web est en UTF-8 et XHTML Transitional, utilisez : + +<?php + require_once('/path/to/htmlpurifier/library/HTMLPurifier.auto.php'); + $purificateur = new HTMLPurifier(); + $html_propre = $purificateur->purify($html_a_purifier); +?> + +Sinon, utilisez : + +<?php + require_once('/path/to/html/purifier/library/HTMLPurifier.auto.load'); + $config = $HTMLPurifier_Config::createDefault(); + $config->set('Core', 'Encoding', 'ISO-8859-1'); //Remplacez par votre + encodage + $config->set('Core', 'XHTML', true); //Remplacer par false si HTML 4.01 + $purificateur = new HTMLPurifier($config); + $html_propre = $purificateur->purify($html_a_purifier); +?> + + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/LICENSE b/vendor/ezyang/htmlpurifier/LICENSE new file mode 100644 index 000000000..8c88a20d4 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/LICENSE @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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 + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/NEWS b/vendor/ezyang/htmlpurifier/NEWS new file mode 100644 index 000000000..82ebedf3e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/NEWS @@ -0,0 +1,1168 @@ +NEWS ( CHANGELOG and HISTORY ) HTMLPurifier +||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + += KEY ==================== + # Breaks back-compat + ! Feature + - Bugfix + + Sub-comment + . Internal change +========================== + +4.9.2, released 2017-03-12 +- Fixes PHP 5.3 compatibility +- Fix breakage when decoding decimal entities. Thanks @rybakit (#129) + +4.9.1, released 2017-03-08 +! %URI.DefaultScheme can now be set to null, in which case + all relative paths are removed. +! New CSS properties: min-width, max-width, min-height, max-height (#94) +! Transparency (rgba) and hsl/hsla supported where color CSS is present. + Thanks @fxbt for contributing the patch. (#118) +- When idn_to_ascii is defined, we might accept malformed + hostnames. Apply validation to the result in such cases. +- Close directory when done in Serializer DefinitionCache (#100) +- Deleted some asserts to avoid linters from choking (#97) +- Rework Serializer cache behavior to avoid chmod'ing if possible (#32) +- Embedded semicolons in strings in CSS are now handled correctly! +- We accidentally dropped certain Unicode characters if there was + one or more invalid characters. This has been fixed, thanks + to mpyw <ryosuke_i_628@yahoo.co.jp> +- Fix for "Don't truncate upon encountering </div> when using DOMLex" + caused a regression with HTML 4.01 Strict parsing with libxml 2.9.1 + (and maybe later versions, but known OK with libxml 2.9.4). The + fix is to go about handling truncation a bit more cleverly so that + we can wrap with divs (sidestepping the bug) but slurping out the + rest of the text in case it ran off the end. (#78) +- Fix PREG_BACKTRACK_LIMIT_ERROR in HTMLPurifier_Filter_ExtractStyle. + Thanks @breathbath for contributing the report and fix (#120) +- Fix entity decoding algorithm to be more conservative about + decoding entities that are missing trailing semicolon. + To get old behavior, set %Core.LegacyEntityDecoder to true. + (#119) +- Workaround libxml bug when HTML tags are embedded inside + script tags. To disable workaround set %Core.AggressivelyRemoveScript + to false. (#83) +# By default, when a link has a target attribute associated + with it, we now also add rel="noopener" in order to + prevent the new window from being able to overwrite + the original frame. To disable this protection, + set %HTML.TargetNoopener to FALSE. + +4.9.0 was cut on Git but never properly released; when we did the +real release we decided to skip this version number. + +4.8.0, released 2016-07-16 +# By default, when a link has a target attribute associated + with it, we now also add rel="noreferrer" in order to + prevent the new window from being able to overwrite + the original frame. To disable this protection, + set %HTML.TargetNoreferrer to FALSE. +! Full PHP 7 compatibility, the test suite is ALL GO. +! %CSS.AllowDuplicates permits duplicate CSS properties. +! Support for 'tel' URIs. +! Partial support for 'border-radius' properties when %CSS.AllowProprietary is true. + The slash syntax, i.e., 'border-radius: 2em 1em 4em / 0.5em 3em' is not + yet supported. +! %Attr.ID.HTML5 turns on HTML5-style ID handling. +- alt truncation could result in malformed UTF-8 sequence. Don't + truncate. Thanks Brandon Farber for reporting. +- Linkify regex is smarter, based off of Gruber's regex. +- IDNA supported natively on PHP 5.3 and later. +- Non all-numeric top-level names (e.g., foo.1f, 1f) are now + allowed. +- Minor bounds error fix to squash a PHP 7 notice. +- Support non-/tmp temporary directories for data:// validation +- Give a better error message when a user attempts to allow + ul/ol without allowing li. +- On some versions of PHP, the Serializer DefinitionCache could + infinite loop when the directory exists but is not listable. (#49) +- Don't match for <body> inside comments with + %Core.ConvertDocumentToFragment. (#67) +- SafeObject is now less case sensitive. (#57) +- AutoFormat.RemoveEmpty.Predicate now correctly renders in + web form. (#85) + +4.7.0, released 2015-08-04 +# opacity is now considered a "tricky" CSS property rather than a + proprietary one. +! %AutoFormat.RemoveEmpty.Predicate for specifying exactly when + an element should be considered "empty" (maybe preserve if it + has attributes), and modify iframe support so that the iframe + is removed if it is missing a src attribute. Thanks meeva for + reporting. +- Don't truncate upon encountering </div> when using DOMLex. Thanks + Myrto Christina for finally convincing me to fix this. +- Update YouTube filter for new code. +- Fix parsing of rgb() values with spaces in them for 'border' + attribute. +- Don't remove foo="" attributes if foo is a boolean attribute. Thanks + valME for reporting. + +4.6.0, released 2013-11-30 +# Secure URI munge hashing algorithm has changed to hash_hmac("sha256", $url, $secret). + Please update any verification scripts you may have. +# URI parsing algorithm was made more strict, so only prefixes which + looks like schemes will actually be schemes. Thanks + Michael Gusev <mgusev@sugarcrm.com> for fixing. +# %Core.EscapeInvalidChildren is no longer supported, and no longer does + anything. +! New directive %Core.AllowHostnameUnderscore which allows underscores + in hostnames. +- Eliminate quadratic behavior in DOMLex by using a proper queue. + Thanks Ole Laursen for noticing this. +- Rewritten MakeWellFormed/FixNesting implementation eliminates quadratic + behavior in the rest of the purificaiton pipeline. Thanks Chedburn + Networks for sponsoring this work. +- Made Linkify URL parser a bit less permissive, so that non-breaking + spaces and commas are not included as part of URL. Thanks nAS for fixing. +- Fix some bad interactions with %HTML.Allowed and injectors. Thanks + David Hirtz for reporting. +- Fix infinite loop in DirectLex. Thanks Ashar Javed (@soaj1664ashar) + for reporting. + +4.5.0, released 2013-02-17 +# Fix bug where stacked attribute transforms clobber each other; + this also means it's no longer possible to override attribute + transforms in later modules. No internal code was using this + but this may break some clients. +# We now use SHA-1 to identify cached definitions, instead of MD5. +! Support display:inline-block +! Support for more white-space CSS values. +! Permit underscores in font families +! Support for page-break-* CSS3 properties when proprietary properties + are enabled. +! New directive %Core.DisableExcludes; can be set to 'true' to turn off + SGML excludes checking. If HTML Purifier is removing too much text + and you don't care about full standards compliance, try setting this to + 'true'. +- Use prepend for SPL autoloading on PHP 5.3 and later. +- Fix bug with nofollow transform when pre-existing rel exists. +- Fix bug where background:url() always gets lower-cased + (but not background-image:url()) +- Fix bug with non lower-case color names in HTML +- Fix bug where data URI validation doesn't remove temporary files. + Thanks Javier Marín Ros <javiermarinros@gmail.com> for reporting. +- Don't remove certain empty tags on RemoveEmpty. + +4.4.0, released 2012-01-18 +# Removed PEARSax3 handler. +# URI.Munge now munges URIs inside the same host that go from https + to http. Reported by Neike Taika-Tessaro. +# Core.EscapeNonASCIICharacters now always transforms entities to + entities, even if target encoding is UTF-8. +# Tighten up selector validation in ExtractStyleBlocks. + Non-syntactically valid selectors are now rejected, along with + some of the more obscure ones such as attribute selectors, the + :lang pseudoselector, and anything not in CSS2.1. Furthermore, + ID and class selectors now work properly with the relevant + configuration attributes. Also, mute errors when parsing CSS + with CSS Tidy. Reported by Mario Heiderich and Norman Hippert. +! Added support for 'scope' attribute on tables. +! Added %HTML.TargetBlank, which adds target="blank" to all outgoing links. +! Properly handle sub-lists directly nested inside of lists in + a standards compliant way, by moving them into the preceding <li> +! Added %HTML.AllowedComments and %HTML.AllowedCommentsRegexp for + limited allowed comments in untrusted situations. +! Implement iframes, and allow them to be used in untrusted mode with + %HTML.SafeIframe and %URI.SafeIframeRegexp. Thanks Bradley M. Froehle + <brad.froehle@gmail.com> for submitting an initial version of the patch. +! The Forms module now works properly for transitional doctypes. +! Added support for internationalized domain names. You need the PEAR + Net_IDNA2 module to be in your path; if it is installed, ensure the + class can be loaded and then set %Core.EnableIDNA to true. +- Color keywords are now case insensitive. Thanks Yzmir Ramirez + <yramirez-htmlpurifier@adicio.com> for reporting. +- Explicitly initialize anonModule variable to null. +- Do not duplicate nofollow if already present. Thanks 178 + for reporting. +- Do not add nofollow if hostname matches our current host. Thanks 178 + for reporting, and Neike Taika-Tessaro for helping diagnose. +- Do not unset parser variable; this fixes intermittent serialization + problems. Thanks Neike Taika-Tessaro for reporting, bill + <10010tiger@gmail.com> for diagnosing. +- Fix iconv truncation bug, where non-UTF-8 target encodings see + output truncated after around 8000 characters. Thanks Jörg Ludwig + <joerg.ludwig@iserv.eu> for reporting. +- Fix broken table content model for XHTML1.1 (and also earlier + versions, although the W3C validator doesn't catch those violations). + Thanks GlitchMr <glitch.mr@gmail.com> for reporting. + +4.3.0, released 2011-03-27 +# Fixed broken caching of customized raw definitions, but requires an + API change. The old API still works but will emit a warning, + see http://htmlpurifier.org/docs/enduser-customize.html#optimized + for how to upgrade your code. +# Protect against Internet Explorer innerHTML behavior by specially + treating attributes with backticks but no angled brackets, quotes or + spaces. This constitutes a slight semantic change, which can be + reverted using %Output.FixInnerHTML. Reported by Neike Taika-Tessaro + and Mario Heiderich. +# Protect against cssText/innerHTML by restricting allowed characters + used in fonts further than mandated by the specification and encoding + some extra special characters in URLs. Reported by Neike + Taika-Tessaro and Mario Heiderich. +! Added %HTML.Nofollow to add rel="nofollow" to external links. +! More types of SPL autoloaders allowed on later versions of PHP. +! Implementations for position, top, left, right, bottom, z-index + when %CSS.Trusted is on. +! Add %Cache.SerializerPermissions option for custom serializer + directory/file permissions +! Fix longstanding bug in Flash support for non-IE browsers, and + allow more wmode attributes. +! Add %CSS.AllowedFonts to restrict permissible font names. +- Switch to an iterative traversal of the DOM, which prevents us + from running out of stack space for deeply nested documents. + Thanks Maxim Krizhanovsky for contributing a patch. +- Make removal of conditional IE comments ungreedy; thanks Bernd + for reporting. +- Escape CDATA before removing Internet Explorer comments. +- Fix removal of id attributes under certain conditions by ensuring + armor attributes are preserved when recreating tags. +- Check if schema.ser was corrupted. +- Check if zend.ze1_compatibility_mode is on, and error out if it is. + This safety check is only done for HTMLPurifier.auto.php; if you + are using standalone or the specialized includes files, you're + expected to know what you're doing. +- Stop repeatedly writing the cache file after I'm done customizing a + raw definition. Reported by ajh. +- Switch to using require_once in the Bootstrap to work around bad + interaction with Zend Debugger and APC. Reported by Antonio Parraga. +- Fix URI handling when hostname is missing but scheme is present. + Reported by Neike Taika-Tessaro. +- Fix missing numeric entities on DirectLex; thanks Neike Taika-Tessaro + for reporting. +- Fix harmless notice from indexing into empty string. Thanks Matthijs + Kooijman <matthijs@stdin.nl> for reporting. +- Don't autoclose no parent elements are able to support the element + that triggered the autoclose. In particular fixes strange behavior + of stray <li> tags. Thanks pkuliga@gmail.com for reporting and + Neike Taika-Tessaro <pinkgothic@gmail.com> for debugging assistance. + +4.2.0, released 2010-09-15 +! Added %Core.RemoveProcessingInstructions, which lets you remove + <? ... ?> statements. +! Added %URI.DisableResources functionality; the directive originally + did nothing. Thanks David Rothstein for reporting. +! Add documentation about configuration directive types. +! Add %CSS.ForbiddenProperties configuration directive. +! Add %HTML.FlashAllowFullScreen to permit embedded Flash objects + to utilize full-screen mode. +! Add optional support for the <code>file</code> URI scheme, enable + by explicitly setting %URI.AllowedSchemes. +! Add %Core.NormalizeNewlines options to allow turning off newline + normalization. +- Fix improper handling of Internet Explorer conditional comments + by parser. Thanks zmonteca for reporting. +- Fix missing attributes bug when running on Mac Snow Leopard and APC. + Thanks sidepodcast for the fix. +- Warn if an element is allowed, but an attribute it requires is + not allowed. + +4.1.1, released 2010-05-31 +- Fix undefined index warnings in maintenance scripts. +- Fix bug in DirectLex for parsing elements with a single attribute + with entities. +- Rewrite CSS output logic for font-family and url(). Thanks Mario + Heiderich <mario.heiderich@googlemail.com> for reporting and Takeshi + Terada <t-terada@violet.plala.or.jp> for suggesting the fix. +- Emit an error for CollectErrors if a body is extracted +- Fix bug where in background-position for center keyword handling. +- Fix infinite loop when a wrapper element is inserted in a context + where it's not allowed. Thanks Lars <lars@renoz.dk> for reporting. +- Remove +x bit and shebang from index.php; only supported mode is to + explicitly call it with php. +- Make test script less chatty when log_errors is on. + +4.1.0, released 2010-04-26 +! Support proprietary height attribute on table element +! Support YouTube slideshows that contain /cp/ in their URL. +! Support for data: URI scheme; not enabled by default, add it using + %URI.AllowedSchemes +! Support flashvars when using %HTML.SafeObject and %HTML.SafeEmbed. +! Support for Internet Explorer compatibility with %HTML.SafeObject + using %Output.FlashCompat. +! Handle <ol><ol> properly, by inserting the necessary <li> tag. +- Always quote the insides of url(...) in CSS. + +4.0.0, released 2009-07-07 +# APIs for ConfigSchema subsystem have substantially changed. See + docs/dev-config-bcbreaks.txt for details; in essence, anything that + had both namespace and directive now have a single unified key. +# Some configuration directives were renamed, specifically: + %AutoFormatParam.PurifierLinkifyDocURL -> %AutoFormat.PurifierLinkify.DocURL + %FilterParam.ExtractStyleBlocksEscaping -> %Filter.ExtractStyleBlocks.Escaping + %FilterParam.ExtractStyleBlocksScope -> %Filter.ExtractStyleBlocks.Scope + %FilterParam.ExtractStyleBlocksTidyImpl -> %Filter.ExtractStyleBlocks.TidyImpl + As usual, the old directive names will still work, but will throw E_NOTICE + errors. +# The allowed values for class have been relaxed to allow all of CDATA for + doctypes that are not XHTML 1.1 or XHTML 2.0. For old behavior, set + %Attr.ClassUseCDATA to false. +# Instead of appending the content model to an old content model, a blank + element will replace the old content model. You can use #SUPER to get + the old content model. +! More robust support for name="" and id="" +! HTMLPurifier_Config::inherit($config) allows you to inherit one + configuration, and have changes to that configuration be propagated + to all of its children. +! Implement %HTML.Attr.Name.UseCDATA, which relaxes validation rules on + the name attribute when set. Use with care. Thanks Ian Cook for + sponsoring. +! Implement %AutoFormat.RemoveEmpty.RemoveNbsp, which removes empty + tags that contain non-breaking spaces as well other whitespace. You + can also modify which tags should have maintained with + %AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions. +! Implement %Attr.AllowedClasses, which allows administrators to restrict + classes users can use to a specified finite set of classes, and + %Attr.ForbiddenClasses, which is the logical inverse. +! You can now maintain your own configuration schema directories by + creating a config-schema.php file or passing an extra argument. Check + docs/dev-config-schema.html for more details. +! Added HTMLPurifier_Config->serialize() method, which lets you save away + your configuration in a compact serial file, which you can unserialize + and use directly without having to go through the overhead of setup. +- Fix bug where URIDefinition would not get cleared if it's directives got + changed. +- Fix fatal error in HTMLPurifier_Encoder on certain platforms (probably NetBSD 5.0) +- Fix bug in Linkify autoformatter involving <a><span>http://foo</span></a> +- Make %URI.Munge not apply to links that have the same host as your host. +- Prevent stray </body> tag from truncating output, if a second </body> + is present. +. Created script maintenance/rename-config.php for renaming a configuration + directive while maintaining its alias. This script does not change source code. +. Implement namespace locking for definition construction, to prevent + bugs where a directive is used for definition construction but is not + used to construct the cache hash. + +3.3.0, released 2009-02-16 +! Implement CSS property 'overflow' when %CSS.AllowTricky is true. +! Implement generic property list classess +- Fix bug with testEncodingSupportsASCII() algorithm when iconv() implementation + does not do the "right thing" with characters not supported in the output + set. +- Spellcheck UTF-8: The Secret To Character Encoding +- Fix improper removal of the contents of elements with only whitespace. Thanks + Eric Wald for reporting. +- Fix broken test suite in versions of PHP without spl_autoload_register() +- Fix degenerate case with YouTube filter involving double hyphens. + Thanks Pierre Attar for reporting. +- Fix YouTube rendering problem on certain versions of Firefox. +- Fix CSSDefinition Printer problems with decorators +- Add text parameter to unit tests, forces text output +. Add verbose mode to command line test runner, use (--verbose) +. Turn on unit tests for UnitConverter +. Fix missing version number in configuration %Attr.DefaultImageAlt (added 3.2.0) +. Fix newline errors that caused spurious failures when CRLF HTML Purifier was + tested on Linux. +. Removed trailing whitespace from all text files, see + remote-trailing-whitespace.php maintenance script. +. Convert configuration to use property list backend. + +3.2.0, released 2008-10-31 +# Using %Core.CollectErrors forces line number/column tracking on, whereas + previously you could theoretically turn it off. +# HTMLPurifier_Injector->notifyEnd() is formally deprecated. Please + use handleEnd() instead. +! %Output.AttrSort for when you need your attributes in alphabetical order to + deal with a bug in FCKEditor. Requested by frank farmer. +! Enable HTML comments when %HTML.Trusted is on. Requested by Waldo Jaquith. +! Proper support for name attribute. It is now allowed and equivalent to the id + attribute in a and img tags, and is only converted to id when %HTML.TidyLevel + is heavy (for all doctypes). +! %AutoFormat.RemoveEmpty to remove some empty tags from documents. Please don't + use on hand-written HTML. +! Add error-cases for unsupported elements in MakeWellFormed. This enables + the strategy to be used, standalone, on untrusted input. +! %Core.AggressivelyFixLt is on by default. This causes more sensible + processing of left angled brackets in smileys and other whatnot. +! Test scripts now have a 'type' parameter, which lets you say 'htmlpurifier', + 'phpt', 'vtest', etc. in order to only execute those tests. This supercedes + the --only-phpt parameter, although for backwards-compatibility the flag + will still work. +! AutoParagraph auto-formatter will now preserve double-newlines upon output. + Users who are not performing inbound filtering, this may seem a little + useless, but as a bonus, the test suite and handling of edge cases is also + improved. +! Experimental implementation of forms for %HTML.Trusted +! Track column numbers when maintain line numbers is on +! Proprietary 'background' attribute on table-related elements converted into + corresponding CSS. Thanks Fusemail for sponsoring this feature! +! Add forward(), forwardUntilEndToken(), backward() and current() to Injector + supertype. +! HTMLPurifier_Injector->handleEnd() permits modification to end tokens. The + time of operation varies slightly from notifyEnd() as *all* end tokens are + processed by the injector before they are subject to the well-formedness rules. +! %Attr.DefaultImageAlt allows overriding default behavior of setting alt to + basename of image when not present. +! %AutoFormat.DisplayLinkURI neuters <a> tags into plain text URLs. +- Fix two bugs in %URI.MakeAbsolute; one involving empty paths in base URLs, + the other involving an undefined $is_folder error. +- Throw error when %Core.Encoding is set to a spurious value. Previously, + this errored silently and returned false. +- Redirected stderr to stdout for flush error output. +- %URI.DisableExternal will now use the host in %URI.Base if %URI.Host is not + available. +- Do not re-munge URL if the output URL has the same host as the input URL. + Requested by Chris. +- Fix error in documentation regarding %Filter.ExtractStyleBlocks +- Prevent <![CDATA[<body></body>]]> from triggering %Core.ConvertDocumentToFragment +- Fix bug with inline elements in blockquotes conflicting with strict doctype +- Detect if HTML support is disabled for DOM by checking for loadHTML() method. +- Fix bug where dots and double-dots in absolute URLs without hostname were + not collapsed by URIFilter_MakeAbsolute. +- Fix bug with anonymous modules operating on SafeEmbed or SafeObject elements + by reordering their addition. +- Will now throw exception on many error conditions during lexer creation; also + throw an exception when MaintainLineNumbers is true, but a non-tracksLineNumbers + is being used. +- Detect if domxml extension is loaded, and use DirectLEx accordingly. +- Improve handling of big numbers with floating point arithmetic in UnitConverter. + Reported by David Morton. +. Strategy_MakeWellFormed now operates in-place, saving memory and allowing + for more interesting filter-backtracking +. New HTMLPurifier_Injector->rewind() functionality, allows injectors to rewind + index to reprocess tokens. +. StringHashParser now allows for multiline sections with "empty" content; + previously the section would remain undefined. +. Added --quick option to multitest.php, which tests only the most recent + release for each series. +. Added --distro option to multitest.php, which accepts either 'normal' or + 'standalone'. This supercedes --exclude-normal and --exclude-standalone + +3.1.1, released 2008-06-19 +# %URI.Munge now, by default, does not munge resources (for example, <img src="">) + In order to enable this again, please set %URI.MungeResources to true. +! More robust imagecrash protection with height/width CSS with %CSS.MaxImgLength, + and height/width HTML with %HTML.MaxImgLength. +! %URI.MungeSecretKey for secure URI munging. Thanks Chris + for sponsoring this feature. Check out the corresponding documentation + for details. (Att Nightly testers: The API for this feature changed before + the general release. Namely, rename your directives %URI.SecureMungeSecretKey => + %URI.MungeSecretKey and and %URI.SecureMunge => %URI.Munge) +! Implemented post URI filtering. Set member variable $post to true to set + a URIFilter as such. +! Allow modules to define injectors via $info_injector. Injectors are + automatically disabled if injector's needed elements are not found. +! Support for "safe" objects added, use %HTML.SafeObject and %HTML.SafeEmbed. + Thanks Chris for sponsoring. If you've been using ad hoc code from the + forums, PLEASE use this instead. +! Added substitutions for %e, %n, %a and %p in %URI.Munge (in order, + embedded, tag name, attribute name, CSS property name). See %URI.Munge + for more details. Requested by Jochem Blok. +- Disable percent height/width attributes for img. +- AttrValidator operations are now atomic; updates to attributes are not + manifest in token until end of operations. This prevents naughty internal + code from directly modifying CurrentToken when they're not supposed to. + This semantics change was requested by frank farmer. +- Percent encoding checks enabled for URI query and fragment +- Fix stray backslashes in font-family; CSS Unicode character escapes are + now properly resolved (although *only* in font-family). Thanks Takeshi Terada + for reporting. +- Improve parseCDATA algorithm to take into account newline normalization +- Account for browser confusion between Yen character and backslash in + Shift_JIS encoding. This fix generalizes to any other encoding which is not + a strict superset of printable ASCII. Thanks Takeshi Terada for reporting. +- Fix missing configuration parameter in Generator calls. Thanks vs for the + partial patch. +- Improved adherence to Unicode by checking for non-character codepoints. + Thanks Geoffrey Sneddon for reporting. This may result in degraded + performance for extremely large inputs. +- Allow CSS property-value pair ''text-decoration: none''. Thanks Jochem Blok + for reporting. +. Added HTMLPurifier_UnitConverter and HTMLPurifier_Length for convenient + handling of CSS-style lengths. HTMLPurifier_AttrDef_CSS_Length now uses + this class. +. API of HTMLPurifier_AttrDef_CSS_Length changed from __construct($disable_negative) + to __construct($min, $max). __construct(true) is equivalent to + __construct('0'). +. Added HTMLPurifier_AttrDef_Switch class +. Rename HTMLPurifier_HTMLModule_Tidy->construct() to setup() and bubble method + up inheritance hierarchy to HTMLPurifier_HTMLModule. All HTMLModules + get this called with the configuration object. All modules now + use this rather than __construct(), although legacy code using constructors + will still work--the new format, however, lets modules access the + configuration object for HTML namespace dependant tweaks. +. AttrDef_HTML_Pixels now takes a single construction parameter, pixels. +. ConfigSchema data-structure heavily optimized; on average it uses a third + the memory it did previously. The interface has changed accordingly, + consult changes to HTMLPurifier_Config for details. +. Variable parsing types now are magic integers instead of strings +. Added benchmark for ConfigSchema +. HTMLPurifier_Generator requires $config and $context parameters. If you + don't know what they should be, use HTMLPurifier_Config::createDefault() + and new HTMLPurifier_Context(). +. Printers now properly distinguish between output configuration, and + target configuration. This is not applicable to scripts using + the Printers for HTML Purifier related tasks. +. HTML/CSS Printers must be primed with prepareGenerator($gen_config), otherwise + fatal errors will ensue. +. URIFilter->prepare can return false in order to abort loading of the filter +. Factory for AttrDef_URI implemented, URI#embedded to indicate URI that embeds + an external resource. +. %URI.Munge functionality factored out into a post-filter class. +. Added CurrentCSSProperty context variable during CSS validation + +3.1.0, released 2008-05-18 +# Unnecessary references to objects (vestiges of PHP4) removed from method + signatures. The following methods do not need references when assigning from + them and will result in E_STRICT errors if you try: + + HTMLPurifier_Config->get*Definition() [* = HTML, CSS] + + HTMLPurifier_ConfigSchema::instance() + + HTMLPurifier_DefinitionCacheFactory::instance() + + HTMLPurifier_DefinitionCacheFactory->create() + + HTMLPurifier_DoctypeRegistry->register() + + HTMLPurifier_DoctypeRegistry->get() + + HTMLPurifier_HTMLModule->addElement() + + HTMLPurifier_HTMLModule->addBlankElement() + + HTMLPurifier_LanguageFactory::instance() +# Printer_ConfigForm's get*() functions were static-ified +# %HTML.ForbiddenAttributes requires attribute declarations to be in the + form of tag@attr, NOT tag.attr (which will throw an error and won't do + anything). This is for forwards compatibility with XML; you'd do best + to migrate an %HTML.AllowedAttributes directives to this syntax too. +! Allow index to be false for config from form creation +! Added HTMLPurifier::VERSION constant +! Commas, not dashes, used for serializer IDs. This change is forwards-compatible + and allows for version numbers like "3.1.0-dev". +! %HTML.Allowed deals gracefully with whitespace anywhere, anytime! +! HTML Purifier's URI handling is a lot more robust, with much stricter + validation checks and better percent encoding handling. Thanks Gareth Heyes + for indicating security vulnerabilities from lax percent encoding. +! Bootstrap autoloader deals more robustly with classes that don't exist, + preventing class_exists($class, true) from barfing. +- InterchangeBuilder now alphabetizes its lists +- Validation error in configdoc output fixed +- Iconv and other encoding errors muted even with custom error handlers that + do not honor error_reporting +- Add protection against imagecrash attack with CSS height/width +- HTMLPurifier::instance() created for consistency, is equivalent to getInstance() +- Fixed and revamped broken ConfigForm smoketest +- Bug with bool/null fields in Printer_ConfigForm fixed +- Bug with global forbidden attributes fixed +- Improved error messages for allowed and forbidden HTML elements and attributes +- Missing (or null) in configdoc documentation restored +- If DOM throws and exception during parsing with PH5P (occurs in newer versions + of DOM), HTML Purifier punts to DirectLex +- Fatal error with unserialization of ScriptRequired +- Created directories are now chmod'ed properly +- Fixed bug with fallback languages in LanguageFactory +- Standalone testing setup properly with autoload +. Out-of-date documentation revised +. UTF-8 encoding check optimization as suggested by Diego +. HTMLPurifier_Error removed in favor of exceptions +. More copy() function removed; should use clone instead +. More extensive unit tests for HTMLDefinition +. assertPurification moved to central harness +. HTMLPurifier_Generator accepts $config and $context parameters during + instantiation, not runtime +. Double-quotes outside of attribute values are now unescaped + +3.1.0rc1, released 2008-04-22 +# Autoload support added. Internal require_once's removed in favor of an + explicit require list or autoloading. To use HTML Purifier, + you must now either use HTMLPurifier.auto.php + or HTMLPurifier.includes.php; setting the include path and including + HTMLPurifier.php is insufficient--in such cases include HTMLPurifier.autoload.php + as well to register our autoload handler (or modify your autoload function + to check HTMLPurifier_Bootstrap::getPath($class)). You can also use + HTMLPurifier.safe-includes.php for a less performance friendly but more + user-friendly library load. +# HTMLPurifier_ConfigSchema static functions are officially deprecated. Schema + information is stored in the ConfigSchema directory, and the + maintenance/generate-schema-cache.php generates the schema.ser file, which + is now instantiated. Support for userland schema changes coming soon! +# HTMLPurifier_Config will now throw E_USER_NOTICE when you use a directive + alias; to get rid of these errors just modify your configuration to use + the new directive name. +# HTMLPurifier->addFilter is deprecated; built-in filters can now be + enabled using %Filter.$filter_name or by setting your own filters using + %Filter.Custom +# Directive-level safety properties superceded in favor of module-level + safety. Internal method HTMLModule->addElement() has changed, although + the externally visible HTMLDefinition->addElement has *not* changed. +! Extra utility classes for testing and non-library operations can + be found in extras/. Specifically, these are FSTools and ConfigDoc. + You may find a use for these in your own project, but right now they + are highly experimental and volatile. +! Integration with PHPT allows for automated smoketests +! Limited support for proprietary HTML elements, namely <marquee>, sponsored + by Chris. You can enable them with %HTML.Proprietary if your client + demands them. +! Support for !important CSS cascade modifier. By default, this will be stripped + from CSS, but you can enable it using %CSS.AllowImportant +! Support for display and visibility CSS properties added, set %CSS.AllowTricky + to true to use them. +! HTML Purifier now has its own Exception hierarchy under HTMLPurifier_Exception. + Developer error (not enduser error) can cause these to be triggered. +! Experimental kses() wrapper introduced with HTMLPurifier.kses.php +! Finally %CSS.AllowedProperties for tweaking allowed CSS properties without + mucking around with HTMLPurifier_CSSDefinition +! ConfigDoc output has been enhanced with version and deprecation info. +! %HTML.ForbiddenAttributes and %HTML.ForbiddenElements implemented. +- Autoclose now operates iteratively, i.e. <span><span><div> now has + both span tags closed. +- Various HTMLPurifier_Config convenience functions now accept another parameter + $schema which defines what HTMLPurifier_ConfigSchema to use besides the + global default. +- Fix bug with trusted script handling in libxml versions later than 2.6.28. +- Fix bug in ExtractStyleBlocks with comments in style tags +- Fix bug in comment parsing for DirectLex +- Flush output now displayed when in command line mode for unit tester +- Fix bug with rgb(0, 1, 2) color syntax with spaces inside shorthand syntax +- HTMLPurifier_HTMLDefinition->addAttribute can now be called multiple times + on the same element without emitting errors. +- Fixed fatal error in PH5P lexer with invalid tag names +. Plugins now get their own changelogs according to project conventions. +. Convert tokens to use instanceof, reducing memory footprint and + improving comparison speed. +. Dry runs now supported in SimpleTest; testing facilities improved +. Bootstrap class added for handling autoloading functionality +. Implemented recursive glob at FSTools->globr +. ConfigSchema now has instance methods for all corresponding define* + static methods. +. A couple of new historical maintenance scripts were added. +. HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php split into two files +. tests/index.php can now be run from any directory. +. HTMLPurifier_Token subclasses split into seperate files +. HTMLPURIFIER_PREFIX now is defined in Bootstrap.php, NOT HTMLPurifier.php +. HTMLPURIFIER_PREFIX can now be defined outside of HTML Purifier +. New --php=php flag added, allows PHP executable to be specified (command + line only!) +. htmlpurifier_add_test() preferred method to translate test files in to + classes, because it handles PHPT files too. +. Debugger class is deprecated and will be removed soon. +. Command line argument parsing for testing scripts revamped, now --opt value + format is supported. +. Smoketests now cleanup after magic quotes +. Generator now can output comments (however, comments are still stripped + from HTML Purifier output) +. HTMLPurifier_ConfigSchema->validate() deprecated in favor of + HTMLPurifier_VarParser->parse() +. Integers auto-cast into float type by VarParser. +. HTMLPURIFIER_STRICT removed; no validation is performed on runtime, only + during cache generation +. Reordered script calls in maintenance/flush.php +. Command line scripts now honor exit codes +. When --flush fails in unit testers, abort tests and print message +. Improved documentation in docs/dev-flush.html about the maintenance scripts +. copy() methods removed in favor of clone keyword + +3.0.0, released 2008-01-06 +# HTML Purifier is PHP 5 only! The 2.1.x branch will be maintained + until PHP 4 is completely deprecated, but no new features will be added + to it. + + Visibility declarations added + + Constructor methods renamed to __construct() + + PHP4 reference cruft removed (in progress) +! CSS properties are now case-insensitive +! DefinitionCacheFactory now can register new implementations +! New HTMLPurifier_Filter_ExtractStyleBlocks for extracting <style> from + documents and cleaning their contents up. Requires the CSSTidy library + <http://csstidy.sourceforge.net/>. You can access the blocks with the + 'StyleBlocks' Context variable ($purifier->context->get('StyleBlocks')). + The output CSS can also be "scoped" for a specific element, use: + %Filter.ExtractStyleBlocksScope +! Experimental support for some proprietary CSS attributes allowed: + opacity (and all of the browser-specific equivalents) and scrollbar colors. + Enable by setting %CSS.Proprietary to true. +- Colors missing # but in hex form will be corrected +- CSS Number algorithm improved +- Unit testing and multi-testing now on steroids: command lines, + XML output, and other goodies now added. +. Unit tests for Injector improved +. New classes: + + HTMLPurifier_AttrDef_CSS_AlphaValue + + HTMLPurifier_AttrDef_CSS_Filter +. Multitest now has a file docblock + +2.1.3, released 2007-11-05 +! tests/multitest.php allows you to test multiple versions by running + tests/index.php through multiple interpreters using `phpv` shell + script (you must provide this script!) +- Fixed poor include ordering for Email URI AttrDefs, causes fatal errors + on some systems. +- Injector algorithm further refined: off-by-one error regarding skip + counts for dormant injectors fixed +- Corrective blockquote definition now enabled for HTML 4.01 Strict +- Fatal error when <img> tag (or any other element with required attributes) + has 'id' attribute fixed, thanks NykO18 for reporting +- Fix warning emitted when a non-supported URI scheme is passed to the + MakeAbsolute URIFilter, thanks NykO18 (again) +- Further refine AutoParagraph injector. Behavior inside of elements + allowing paragraph tags clarified: only inline content delimeted by + double newlines (not block elements) are paragraphed. +- Buggy treatment of end tags of elements that have required attributes + fixed (does not manifest on default tag-set) +- Spurious internal content reorganization error suppressed +- HTMLDefinition->addElement now returns a reference to the created + element object, as implied by the documentation +- Phorum mod's HTML Purifier help message expanded (unreleased elsewhere) +- Fix a theoretical class of infinite loops from DirectLex reported + by Nate Abele +- Work around unnecessary DOMElement type-cast in PH5P that caused errors + in PHP 5.1 +- Work around PHP 4 SimpleTest lack-of-error complaining for one-time-only + HTMLDefinition errors, this may indicate problems with error-collecting + facilities in PHP 5 +- Make ErrorCollectorEMock work in both PHP 4 and PHP 5 +- Make PH5P work with PHP 5.0 by removing unnecessary array parameter typedef +. %Core.AcceptFullDocuments renamed to %Core.ConvertDocumentToFragment + to better communicate its purpose +. Error unit tests can now specify the expectation of no errors. Future + iterations of the harness will be extremely strict about what errors + are allowed +. Extend Injector hooks to allow for more powerful injector routines +. HTMLDefinition->addBlankElement created, as according to the HTMLModule + method +. Doxygen configuration file updated, with minor improvements +. Test runner now checks for similarly named files in conf/ directory too. +. Minor cosmetic change to flush-definition-cache.php: trailing newline is + outputted +. Maintenance script for generating PH5P patch added, original PH5P source + file also added under version control +. Full unit test runner script title made more descriptive with PHP version +. Updated INSTALL file to state that 4.3.7 is the earliest version we + are actively testing + +2.1.2, released 2007-09-03 +! Implemented Object module for trusted users +! Implemented experimental HTML5 parsing mode using PH5P. To use, add + this to your code: + require_once 'HTMLPurifier/Lexer/PH5P.php'; + $config->set('Core', 'LexerImpl', 'PH5P'); + Note that this Lexer introduces some classes not in the HTMLPurifier + namespace. Also, this is PHP5 only. +! CSS property border-spacing implemented +- Fix non-visible parsing error in DirectLex with empty tags that have + slashes inside attribute values. +- Fix typo in CSS definition: border-collapse:seperate; was incorrectly + accepted as valid CSS. Usually non-visible, because this styling is the + default for tables in most browsers. Thanks Brett Zamir for pointing + this out. +- Fix validation errors in configuration form +- Hammer out a bunch of edge-case bugs in the standalone distribution +- Inclusion reflection removed from URISchemeRegistry; you must manually + include any new schema files you wish to use +- Numerous typo fixes in documentation thanks to Brett Zamir +. Unit test refactoring for one logical test per test function +. Config and context parameters in ComplexHarness deprecated: instead, edit + the $config and $context member variables +. HTML wrapper in DOMLex now takes DTD identifiers into account; doesn't + really make a difference, but is good for completeness sake +. merge-library.php script refactored for greater code reusability and + PHP4 compatibility + +2.1.1, released 2007-08-04 +- Fix show-stopper bug in %URI.MakeAbsolute functionality +- Fix PHP4 syntax error in standalone version +. Add prefix directory to include path for standalone, this prevents + other installations from clobbering the standalone's URI schemes +. Single test methods can be invoked by prefixing with __only + +2.1.0, released 2007-08-02 +# flush-htmldefinition-cache.php superseded in favor of a generic + flush-definition-cache.php script, you can clear a specific cache + by passing its name as a parameter to the script +! Phorum mod implemented for HTML Purifier +! With %Core.AggressivelyFixLt, <3 and similar emoticons no longer + trigger HTML removal in PHP5 (DOMLex). This directive is not necessary + for PHP4 (DirectLex). +! Standalone file now available, which greatly reduces the amount of + includes (although there are still a few files that reside in the + standalone folder) +! Relative URIs can now be transformed into their absolute equivalents + using %URI.Base and %URI.MakeAbsolute +! Ruby implemented for XHTML 1.1 +! You can now define custom URI filtering behavior, see enduser-uri-filter.html + for more details +! UTF-8 font names now supported in CSS +- AutoFormatters emit friendly error messages if tags or attributes they + need are not allowed +- ConfigForm's compactification of directive names is now configurable +- AutoParagraph autoformatter algorithm refined after field-testing +- XHTML 1.1 now applies XHTML 1.0 Strict cleanup routines, namely + blockquote wrapping +- Contents of <style> tags removed by default when tags are removed +. HTMLPurifier_Config->getSerial() implemented, this is extremely useful + for output cache invalidation +. ConfigForm printer now can retrieve CSS and JS files as strings, in + case HTML Purifier's directory is not publically accessible +. Introduce new text/itext configuration directive values: these represent + longer strings that would be more appropriately edited with a textarea +. Allow newlines to act as separators for lists, hashes, lookups and + %HTML.Allowed +. ConfigForm generates textareas instead of text inputs for lists, hashes, + lookups, text and itext fields +. Hidden element content removal genericized: %Core.HiddenElements can + be used to customize this behavior, by default <script> and <style> are + hidden +. Added HTMLPURIFIER_PREFIX constant, should be used instead of dirname(__FILE__) +. Custom ChildDef added to default include list +. URIScheme reflection improved: will not attempt to include file if class + already exists. May clobber autoload, so I need to keep an eye on it +. ConfigSchema heavily optimized, will only collect information and validate + definitions when HTMLPURIFIER_SCHEMA_STRICT is true. +. AttrDef_URI unit tests and implementation refactored +. benchmarks/ directory now protected from public view with .htaccess file; + run the tests via command line +. URI scheme is munged off if there is no authority and the scheme is the + default one +. All unit tests inherit from HTMLPurifier_Harness, not UnitTestCase +. Interface for URIScheme changed +. Generic URI object to hold components of URI added, most systems involved + in URI validation have been migrated to use it +. Custom filtering for URIs factored out to URIDefinition interface for + maximum extensibility + +2.0.1, released 2007-06-27 +! Tag auto-closing now based on a ChildDef heuristic rather than a + manually set auto_close array; some behavior may change +! Experimental AutoFormat functionality added: auto-paragraph and + linkify your HTML input by setting %AutoFormat.AutoParagraph and + %AutoFormat.Linkify to true +! Newlines normalized internally, and then converted back to the + value of PHP_EOL. If this is not desired, set your newline format + using %Output.Newline. +! Beta error collection, messages are implemented for the most generic + cases involving Lexing or Strategies +- Clean up special case code for <script> tags +- Reorder includes for DefinitionCache decorators, fixes a possible + missing class error +- Fixed bug where manually modified definitions were not saved via cache + (mostly harmless, except for the fact that it would be a little slower) +- Configuration objects with different serials do not clobber each + others when revision numbers are unequal +- Improve Serializer DefinitionCache directory permissions checks +- DefinitionCache no longer throws errors when it encounters old + serial files that do not conform to the current style +- Stray xmlns attributes removed from configuration documentation +- configForm.php smoketest no longer has XSS vulnerability due to + unescaped print_r output +- Printer adheres to configuration's directives on output format +- Fix improperly named form field in ConfigForm printer +. Rewire some test-cases to swallow errors rather than expect them +. HTMLDefinition printer updated with some of the new attributes +. DefinitionCache keys reordered to reflect precedence: version number, + hash, then revision number +. %Core.DefinitionCache renamed to %Cache.DefinitionImpl +. Interlinking in configuration documentation added using + Injector_PurifierLinkify +. Directives now keep track of aliases to themselves +. Error collector now requires a severity to be passed, use PHP's internal + error constants for this +. HTMLPurifier_Config::getAllowedDirectivesForForm implemented, allows + much easier selective embedding of configuration values +. Doctype objects now accept public and system DTD identifiers +. %HTML.Doctype is now constrained by specific values, to specify a custom + doctype use new %HTML.CustomDoctype +. ConfigForm truncates long directives to keep the form small, and does + not re-output namespaces + +2.0.0, released 2007-06-20 +# Completely refactored HTMLModuleManager, decentralizing safety + information +# Transform modules changed to Tidy modules, which offer more flexibility + and better modularization +# Configuration object now finalizes itself when a read operation is + performed on it, ensuring that its internal state stays consistent. + To revert this behavior, you can set the $autoFinalize member variable + off, but it's not recommended. +# New compact syntax for AttrDef objects that can be used to instantiate + new objects via make() +# Definitions (esp. HTMLDefinition) are now cached for a significant + performance boost. You can disable caching by setting %Core.DefinitionCache + to null. You CANNOT edit raw definitions without setting the corresponding + DefinitionID directive (%HTML.DefinitionID for HTMLDefinition). +# Contents between <script> tags are now completely removed if <script> + is not allowed +# Prototype-declarations for Lexer removed in favor of configuration + determination of Lexer implementations. +! HTML Purifier now works in PHP 4.3.2. +! Configuration form-editing API makes tweaking HTMLPurifier_Config a + breeze! +! Configuration directives that accept hashes now allow new string + format: key1:value1,key2:value2 +! ConfigDoc now factored into OOP design +! All deprecated elements now natively supported +! Implement TinyMCE styled whitelist specification format in + %HTML.Allowed +! Config object gives more friendly error messages when things go wrong +! Advanced API implemented: easy functions for creating elements (addElement) + and attributes (addAttribute) on HTMLDefinition +! Add native support for required attributes +- Deprecated and removed EnableRedundantUTF8Cleaning. It didn't even work! +- DOMLex will not emit errors when a custom error handler that does not + honor error_reporting is used +- StrictBlockquote child definition refrains from wrapping whitespace + in tags now. +- Bug resulting from tag transforms to non-allowed elements fixed +- ChildDef_Custom's regex generation has been improved, removing several + false positives +. Unit test for ElementDef created, ElementDef behavior modified to + be more flexible +. Added convenience functions for HTMLModule constructors +. AttrTypes now has accessor functions that should be used instead + of directly manipulating info +. TagTransform_Center deprecated in favor of generic TagTransform_Simple +. Add extra protection in AttrDef_URI against phantom Schemes +. Doctype object added to HTMLDefinition which describes certain aspects + of the operational document type +. Lexer is now pre-emptively included, with a conditional include for the + PHP5 only version. +. HTMLDefinition and CSSDefinition have a common parent class: Definition. +. DirectLex can now track line-numbers +. Preliminary error collector is in place, although no code actually reports + errors yet +. Factor out most of ValidateAttributes to new AttrValidator class + +1.6.1, released 2007-05-05 +! Support for more deprecated attributes via transformations: + + hspace and vspace in img + + size and noshade in hr + + nowrap in td + + clear in br + + align in caption, table, img and hr + + type in ul, ol and li +! DirectLex now preserves text in which a < bracket is followed by + a non-alphanumeric character. This means that certain emoticons + are now preserved. +! %Core.RemoveInvalidImg is now operational, when set to false invalid + images will hang around with an empty src +! target attribute in a tag supported, use %Attr.AllowedFrameTargets + to enable +! CSS property white-space now allows nowrap (supported in all modern + browsers) but not others (which have spotty browser implementations) +! XHTML 1.1 mode now sort-of works without any fatal errors, and + lang is now moved over to xml:lang. +! Attribute transformation smoketest available at smoketests/attrTransform.php +! Transformation of font's size attribute now handles super-large numbers +- Possibly fatal bug with __autoload() fixed in module manager +- Invert HTMLModuleManager->addModule() processing order to check + prefixes first and then the literal module +- Empty strings get converted to empty arrays instead of arrays with + an empty string in them. +- Merging in attribute lists now works. +. Demo script removed: it has been added to the website's repository +. Basic.php script modified to work out of the box +. Refactor AttrTransform classes to reduce duplication +. AttrTransform_TextAlign axed in favor of a more general + AttrTransform_EnumToCSS, refer to HTMLModule/TransformToStrict.php to + see how the new equivalent is implemented +. Unit tests now use exclusively assertIdentical + +1.6.0, released 2007-04-01 +! Support for most common deprecated attributes via transformations: + + bgcolor in td, th, tr and table + + border in img + + name in a and img + + width in td, th and hr + + height in td, th +! Support for CSS attribute 'height' added +! Support for rel and rev attributes in a tags added, use %Attr.AllowedRel + and %Attr.AllowedRev to activate +- You can define ID blacklists using regular expressions via + %Attr.IDBlacklistRegexp +- Error messages are emitted when you attempt to "allow" elements or + attributes that HTML Purifier does not support +- Fix segfault in unit test. The problem is not very reproduceable and + I don't know what causes it, but a six line patch fixed it. + +1.5.0, released 2007-03-23 +! Added a rudimentary I18N and L10N system modeled off MediaWiki. It + doesn't actually do anything yet, but keep your eyes peeled. +! docs/enduser-utf8.html explains how to use UTF-8 and HTML Purifier +! Newly structured HTMLDefinition modeled off of XHTML 1.1 modules. + I am loathe to release beta quality APIs, but this is exactly that; + don't use the internal interfaces if you're not willing to do migration + later on. +- Allow 'x' subtag in language codes +- Fixed buggy chameleon-support for ins and del +. Added support for IDREF attributes (i.e. for) +. Renamed HTMLPurifier_AttrDef_Class to HTMLPurifier_AttrDef_Nmtokens +. Removed context variable ParentType, replaced with IsInline, which + is false when you're not inline and an integer of the parent that + caused you to become inline when you are (so possibly zero) +. Removed ElementDef->type in favor of ElementDef->descendants_are_inline + and HTMLDefinition->content_sets +. StrictBlockquote now reports what elements its supposed to allow, + rather than what it does allow +. Removed HTMLDefinition->info_flow_elements in favor of + HTMLDefinition->content_sets['Flow'] +. Removed redundant "exclusionary" definitions from DTD roster +. StrictBlockquote now requires a construction parameter as if it + were an Required ChildDef, this is the "real" set of allowed elements +. AttrDef partitioned into HTML, CSS and URI segments +. Modify Youtube filter regexp to be multiline +. Require both PHP5 and DOM extension in order to use DOMLex, fixes + some edge cases where a DOMDocument class exists in a PHP4 environment + due to DOM XML extension. + +1.4.1, released 2007-01-21 +! docs/enduser-youtube.html updated according to new functionality +- YouTube IDs can have underscores and dashes + +1.4.0, released 2007-01-21 +! Implemented list-style-image, URIs now allowed in list-style +! Implemented background-image, background-repeat, background-attachment + and background-position CSS properties. Shorthand property background + supports all of these properties. +! Configuration documentation looks nicer +! Added %Core.EscapeNonASCIICharacters to workaround loss of Unicode + characters while %Core.Encoding is set to a non-UTF-8 encoding. +! Support for configuration directive aliases added +! Config object can now be instantiated from ini files +! YouTube preservation code added to the core, with two lines of code + you can add it as a filter to your code. See smoketests/preserveYouTube.php + for sample code. +! Moved SLOW to docs/enduser-slow.html and added code examples +- Replaced version check with functionality check for DOM (thanks Stephen + Khoo) +. Added smoketest 'all.php', which loads all other smoketests via frames +. Implemented AttrDef_CSSURI for url(http://google.com) style declarations +. Added convenient single test selector form on test runner + +1.3.2, released 2006-12-25 +! HTMLPurifier object now accepts configuration arrays, no need to manually + instantiate a configuration object +! Context object now accessible to outside +! Added enduser-youtube.html, explains how to embed YouTube videos. See + also corresponding smoketest preserveYouTube.php. +! Added purifyArray(), which takes a list of HTML and purifies it all +! Added static member variable $version to HTML Purifier with PHP-compatible + version number string. +- Fixed fatal error thrown by upper-cased language attributes +- printDefinition.php: added labels, added better clarification +. HTMLPurifier_Config::create() added, takes mixed variable and converts into + a HTMLPurifier_Config object. + +1.3.1, released 2006-12-06 +! Added HTMLPurifier.func.php stub for a convenient function to call the library +- Fixed bug in RemoveInvalidImg code that caused all images to be dropped + (thanks to .mario for reporting this) +. Standardized all attribute handling variables to attr, made it plural + +1.3.0, released 2006-11-26 +# Invalid images are now removed, rather than replaced with a dud + <img src="" alt="Invalid image" />. Previous behavior can be restored + with new directive %Core.RemoveInvalidImg set to false. +! (X)HTML Strict now supported + + Transparently handles inline elements in block context (blockquote) +! Added GET method to demo for easier validation, added 50kb max input size +! New directive %HTML.BlockWrapper, for block-ifying inline elements +! New directive %HTML.Parent, allows you to only allow inline content +! New directives %HTML.AllowedElements and %HTML.AllowedAttributes to let + users narrow the set of allowed tags +! <li value="4"> and <ul start="2"> now allowed in loose mode +! New directives %URI.DisableExternalResources and %URI.DisableResources +! New directive %Attr.DisableURI, which eliminates all hyperlinking +! New directive %URI.Munge, munges URI so you can use some sort of redirector + service to avoid PageRank leaks or warn users that they are exiting your site. +! Added spiffy new smoketest printDefinition.php, which lets you twiddle with + the configuration settings and see how the internal rules are affected. +! New directive %URI.HostBlacklist for blocking links to bad hosts. + xssAttacks.php smoketest updated accordingly. +- Added missing type to ChildDef_Chameleon +- Remove Tidy option from demo if there is not Tidy available +. ChildDef_Required guards against empty tags +. Lookup table HTMLDefinition->info_flow_elements added +. Added peace-of-mind variable initialization to Strategy_FixNesting +. Added HTMLPurifier->info_parent_def, parent child processing made special +. Added internal documents briefly summarizing future progression of HTML +. HTMLPurifier_Config->getBatch($namespace) added +. More lenient casting to bool from string in HTMLPurifier_ConfigSchema +. Refactored ChildDef classes into their own files + +1.2.0, released 2006-11-19 +# ID attributes now disabled by default. New directives: + + %HTML.EnableAttrID - restores old behavior by allowing IDs + + %Attr.IDPrefix - %Attr.IDBlacklist alternative that munges all user IDs + so that they don't collide with your IDs + + %Attr.IDPrefixLocal - Same as above, but for when there are multiple + instances of user content on the page + + Profuse documentation on how to use these available in docs/enduser-id.txt +! Added MODx plugin <http://modxcms.com/forums/index.php/topic,6604.0.html> +! Added percent encoding normalization +! XSS attacks smoketest given facelift +! Configuration documentation now has table of contents +! Added %URI.DisableExternal, which prevents links to external websites. You + can also use %URI.Host to permit absolute linking to subdomains +! Non-accessible resources (ex. mailto) blocked from embedded URIs (img src) +- Type variable in HTMLDefinition was not being set properly, fixed +- Documentation updated + + TODO added request Phalanger + + TODO added request Native compression + + TODO added request Remove redundant tags + + TODO added possible plaintext formatter for HTML Purifier documentation + + Updated ConfigDoc TODO + + Improved inline comments in AttrDef/Class.php, AttrDef/CSS.php + and AttrDef/Host.php + + Revamped documentation into HTML, along with misc updates +- HTMLPurifier_Context doesn't throw a variable reference error if you attempt + to retrieve a non-existent variable +. Switched to purify()-wide Context object registry +. Refactored unit tests to minimize duplication +. XSS attack sheet updated +. configdoc.xml now has xml:space attached to default value nodes +. Allow configuration directives to permit null values +. Cleaned up test-cases to remove unnecessary swallowErrors() + +1.1.2, released 2006-09-30 +! Add HTMLPurifier.auto.php stub file that configures include_path +- Documentation updated + + INSTALL document rewritten + + TODO added semi-lossy conversion + + API Doxygen docs' file exclusions updated + + Added notes on HTML versus XML attribute whitespace handling + + Noted that HTMLPurifier_ChildDef_Custom isn't being used + + Noted that config object's definitions are cached versions +- Fixed lack of attribute parsing in HTMLPurifier_Lexer_PEARSax3 +- ftp:// URIs now have their typecodes checked +- Hooked up HTMLPurifier_ChildDef_Custom's unit tests (they weren't being run) +. Line endings standardized throughout project (svn:eol-style standardized) +. Refactored parseData() to general Lexer class +. Tester named "HTML Purifier" not "HTMLPurifier" + +1.1.1, released 2006-09-24 +! Configuration option to optionally Tidy up output for indentation to make up + for dropped whitespace by DOMLex (pretty-printing for the entire application + should be done by a page-wide Tidy) +- Various documentation updates +- Fixed parse error in configuration documentation script +- Fixed fatal error in benchmark scripts, slightly augmented +- As far as possible, whitespace is preserved in-between table children +- Sample test-settings.php file included + +1.1.0, released 2006-09-16 +! Directive documentation generation using XSLT +! XHTML can now be turned off, output becomes <br> +- Made URI validator more forgiving: will ignore leading and trailing + quotes, apostrophes and less than or greater than signs. +- Enforce alphanumeric namespace and directive names for configuration. +- Table child definition made more flexible, will fix up poorly ordered elements +. Renamed ConfigDef to ConfigSchema + +1.0.1, released 2006-09-04 +- Fixed slight bug in DOMLex attribute parsing +- Fixed rejection of case-insensitive configuration values when there is a + set of allowed values. This manifested in %Core.Encoding. +- Fixed rejection of inline style declarations that had lots of extra + space in them. This manifested in TinyMCE. + +1.0.0, released 2006-09-01 +! Shorthand CSS properties implemented: font, border, background, list-style +! Basic color keywords translated into hexadecimal values +! Table CSS properties implemented +! Support for charsets other than UTF-8 (defined by iconv) +! Malformed UTF-8 and non-SGML character detection and cleaning implemented +- Fixed broken numeric entity conversion +- API documentation completed +. (HTML|CSS)Definition de-singleton-ized + +1.0.0beta, released 2006-08-16 +! First public release, most functionality implemented. Notable omissions are: + + Shorthand CSS properties + + Table CSS properties + + Deprecated attribute transformations + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/README.md b/vendor/ezyang/htmlpurifier/README.md new file mode 100644 index 000000000..b321f2b69 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/README.md @@ -0,0 +1,29 @@ +HTML Purifier [![Build Status](https://secure.travis-ci.org/ezyang/htmlpurifier.svg?branch=master)](http://travis-ci.org/ezyang/htmlpurifier) +============= + +HTML Purifier is an HTML filtering solution that uses a unique combination +of robust whitelists and agressive parsing to ensure that not only are +XSS attacks thwarted, but the resulting HTML is standards compliant. + +HTML Purifier is oriented towards richly formatted documents from +untrusted sources that require CSS and a full tag-set. This library can +be configured to accept a more restrictive set of tags, but it won't be +as efficient as more bare-bones parsers. It will, however, do the job +right, which may be more important. + +Places to go: + +* See INSTALL for a quick installation guide +* See docs/ for developer-oriented documentation, code examples and + an in-depth installation guide. +* See WYSIWYG for information on editors like TinyMCE and FCKeditor + +HTML Purifier can be found on the web at: [http://htmlpurifier.org/](http://htmlpurifier.org/) + +## Installation + +Package available on [Composer](https://packagist.org/packages/ezyang/htmlpurifier). + +If you're using Composer to manage dependencies, you can use + + $ composer require "ezyang/htmlpurifier": "dev-master" diff --git a/vendor/ezyang/htmlpurifier/TODO b/vendor/ezyang/htmlpurifier/TODO new file mode 100644 index 000000000..1afb33cbf --- /dev/null +++ b/vendor/ezyang/htmlpurifier/TODO @@ -0,0 +1,150 @@ + +TODO List + += KEY ==================== + # Flagship + - Regular + ? Maybe I'll Do It +========================== + +If no interest is expressed for a feature that may require a considerable +amount of effort to implement, it may get endlessly delayed. Do not be +afraid to cast your vote for the next feature to be implemented! + +Things to do as soon as possible: + + - http://htmlpurifier.org/phorum/read.php?3,5560,6307#msg-6307 + - Think about allowing explicit order of operations hooks for transforms + - Fix "<.<" bug (trailing < is removed if not EOD) + - Build in better internal state dumps and debugging tools for remote + debugging + - Allowed/Allowed* have strange interactions when both set + ? Transform lone embeds into object tags + - Deprecated config options that emit warnings when you set them (with' + a way of muting the warning if you really want to) + - Make HTML.Trusted work with Output.FlashCompat + - HTML.Trusted and HTML.SafeObject have funny interaction; general + problem is what to do when a module "supersedes" another + (see also tables and basic tables.) This is a little dicier + because HTML.SafeObject has some extra functionality that + trusted might find useful. See http://htmlpurifier.org/phorum/read.php?3,5762,6100 + +FUTURE VERSIONS +--------------- + +4.9 release [OMG CONFIG PONIES] + ! Fix Printer. It's from the old days when we didn't have decent XML classes + ! Factor demo.php into a set of Printer classes, and then create a stub + file for users here (inside the actual HTML Purifier library) + - Fix error handling with form construction + - Do encoding validation in Printers, or at least, where user data comes in + - Config: Add examples to everything (make built-in which also automatically + gives output) + - Add "register" field to config schemas to eliminate dependence on + naming conventions (try to remember why we ultimately decided on tihs) + +5.0 release [HTML 5] + # Swap out code to use html5lib tokenizer and tree-builder + ! Allow turning off of FixNesting and required attribute insertion + +5.1 release [It's All About Trust] (floating) + # Implement untrusted, dangerous elements/attributes + # Implement IDREF support (harder than it seems, since you cannot have + IDREFs to non-existent IDs) + - Implement <area> (client and server side image maps are blocking + on IDREF support) + # Frameset XHTML 1.0 and HTML 4.01 doctypes + - Figure out how to simultaneously set %CSS.Trusted and %HTML.Trusted (?) + +5.2 release [Error'ed] + # Error logging for filtering/cleanup procedures + # Additional support for poorly written HTML + - Microsoft Word HTML cleaning (i.e. MsoNormal, but research essential!) + - Friendly strict handling of <address> (block -> <br>) + - XSS-attempt detection--certain errors are flagged XSS-like + - Append something to duplicate IDs so they're still usable (impl. note: the + dupe detector would also need to detect the suffix as well) + +6.0 release [Beyond HTML] + # Legit token based CSS parsing (will require revamping almost every + AttrDef class). Probably will use CSSTidy + # More control over allowed CSS properties using a modularization + # IRI support (this includes IDN) + - Standardize token armor for all areas of processing + +7.0 release [To XML and Beyond] + - Extended HTML capabilities based on namespacing and tag transforms (COMPLEX) + - Hooks for adding custom processors to custom namespaced tags and + attributes, offer default implementation + - Lots of documentation and samples + +Ongoing + - More refactoring to take advantage of PHP5's facilities + - Refactor unit tests into lots of test methods + - Plugins for major CMSes (COMPLEX) + - phpBB + - Also, a FAQ for extension writers with HTML Purifier + +AutoFormat + - Smileys + - Syntax highlighting (with GeSHi) with <pre> and possibly <?php + - Look at http://drupal.org/project/Modules/category/63 for ideas + +Neat feature related + ! Support exporting configuration, so users can easily tweak settings + in the demo, and then copy-paste into their own setup + - Advanced URI filtering schemes (see docs/proposal-new-directives.txt) + - Allow scoped="scoped" attribute in <style> tags; may be troublesome + because regular CSS has no way of uniquely identifying nodes, so we'd + have to generate IDs + - Explain how to use HTML Purifier in non-PHP languages / create + a simple command line stub (or complicated?) + - Fixes for Firefox's inability to handle COL alignment props (Bug 915) + - Automatically add non-breaking spaces to empty table cells when + empty-cells:show is applied to have compatibility with Internet Explorer + - Table of Contents generation (XHTML Compiler might be reusable). May also + be out-of-band information. + - Full set of color keywords. Also, a way to add onto them without + finalizing the configuration object. + - Write a var_export and memcached DefinitionCache - Denis + - Built-in support for target="_blank" on all external links + - Convert RTL/LTR override characters to <bdo> tags, or vice versa on demand. + Also, enable disabling of directionality + ? Externalize inline CSS to promote clean HTML, proposed by Sander Tekelenburg + ? Remove redundant tags, ex. <u><u>Underlined</u></u>. Implementation notes: + 1. Analyzing which tags to remove duplicants + 2. Ensure attributes are merged into the parent tag + 3. Extend the tag exclusion system to specify whether or not the + contents should be dropped or not (currently, there's code that could do + something like this if it didn't drop the inner text too.) + ? Make AutoParagraph also support paragraph-izing double <br> tags, and not + just double newlines. This is kind of tough to do in the current framework, + though, and might be reasonably approximated by search replacing double <br>s + with newlines before running it through HTML Purifier. + +Maintenance related (slightly boring) + # CHMOD install script for PEAR installs + ! Factor out command line parser into its own class, and unit test it + - Reduce size of internal data-structures (esp. HTMLDefinition) + - Allow merging configurations. Thus, + a -> b -> default + c -> d -> default + becomes + a -> b -> c -> d -> default + Maybe allow more fine-grained tuning of this behavior. Alternatively, + encourage people to use short plist depths before building them up. + - Time PHPT tests + +ChildDef related (very boring) + - Abstract ChildDef_BlockQuote to work with all elements that only + allow blocks in them, required or optional + - Implement lenient <ruby> child validation + +Wontfix + - Non-lossy smart alternate character encoding transformations (unless + patch provided) + - Pretty-printing HTML: users can use Tidy on the output on entire page + - Native content compression, whitespace stripping: use gzip if this is + really important + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/VERSION b/vendor/ezyang/htmlpurifier/VERSION new file mode 100644 index 000000000..b550c72a1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/VERSION @@ -0,0 +1 @@ +4.9.2
\ No newline at end of file diff --git a/vendor/ezyang/htmlpurifier/WHATSNEW b/vendor/ezyang/htmlpurifier/WHATSNEW new file mode 100644 index 000000000..b435e664b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/WHATSNEW @@ -0,0 +1,12 @@ +HTML Purifier 4.9.x is a maintenance release, collecting a year +of accumulated bug fixes plus a few new features. New features +include support for min/max-width/height CSS, and rgba/hsl/hsla +in color specifications. Major bugfixes include improvements +in the Serializer cache to avoid chmod'ing directories, better +entity decoding (we won't accidentally encode entities that occur +in URLs) and rel="noopener" on links with target attributes, +to prevent them from overwriting the original frame. + +4.9.0 was skipped due to a packaging problem; 4.9.2 fixes two +major regressions in PHP 5.3 support and entity decoding; no +other functional changes were applied. diff --git a/vendor/ezyang/htmlpurifier/WYSIWYG b/vendor/ezyang/htmlpurifier/WYSIWYG new file mode 100644 index 000000000..c518aacdd --- /dev/null +++ b/vendor/ezyang/htmlpurifier/WYSIWYG @@ -0,0 +1,20 @@ + +WYSIWYG - What You See Is What You Get + HTML Purifier: A Pretty Good Fit for TinyMCE and FCKeditor + +Javascript-based WYSIWYG editors, simply stated, are quite amazing. But I've +always been wary about using them due to security issues: they handle the +client-side magic, but once you've been served a piping hot load of unfiltered +HTML, what should be done then? In some situations, you can serve it uncleaned, +since you only offer these facilities to trusted(?) authors. + +Unfortunantely, for blog comments and anonymous input, BBCode, Textile and +other markup languages still reign supreme. Put simply: filtering HTML is +hard work, and these WYSIWYG authors don't offer anything to alleviate that +trouble. Therein lies the solution: + +HTML Purifier is perfect for filtering pure-HTML input from WYSIWYG editors. + +Enough said. + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/composer.json b/vendor/ezyang/htmlpurifier/composer.json new file mode 100644 index 000000000..80fee3db3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/composer.json @@ -0,0 +1,25 @@ +{ + "name": "ezyang/htmlpurifier", + "description": "Standards compliant HTML filter written in PHP", + "type": "library", + "keywords": ["html"], + "homepage": "http://htmlpurifier.org/", + "license": "LGPL", + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "require": { + "php": ">=5.2" + }, + "require-dev": { + "simpletest/simpletest": "^1.1" + }, + "autoload": { + "psr-0": { "HTMLPurifier": "library/" }, + "files": ["library/HTMLPurifier.composer.php"] + } +} diff --git a/vendor/ezyang/htmlpurifier/extras/ConfigDoc/HTMLXSLTProcessor.php b/vendor/ezyang/htmlpurifier/extras/ConfigDoc/HTMLXSLTProcessor.php new file mode 100644 index 000000000..1cfec5d76 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/extras/ConfigDoc/HTMLXSLTProcessor.php @@ -0,0 +1,91 @@ +<?php + +/** + * Decorator/extender XSLT processor specifically for HTML documents. + */ +class ConfigDoc_HTMLXSLTProcessor +{ + + /** + * Instance of XSLTProcessor + */ + protected $xsltProcessor; + + public function __construct($proc = false) + { + if ($proc === false) $proc = new XSLTProcessor(); + $this->xsltProcessor = $proc; + } + + /** + * @note Allows a string $xsl filename to be passed + */ + public function importStylesheet($xsl) + { + if (is_string($xsl)) { + $xsl_file = $xsl; + $xsl = new DOMDocument(); + $xsl->load($xsl_file); + } + return $this->xsltProcessor->importStylesheet($xsl); + } + + /** + * Transforms an XML file into compatible XHTML based on the stylesheet + * @param $xml XML DOM tree, or string filename + * @return string HTML output + * @todo Rename to transformToXHTML, as transformToHTML is misleading + */ + public function transformToHTML($xml) + { + if (is_string($xml)) { + $dom = new DOMDocument(); + $dom->load($xml); + } else { + $dom = $xml; + } + $out = $this->xsltProcessor->transformToXML($dom); + + // fudges for HTML backwards compatibility + // assumes that document is XHTML + $out = str_replace('/>', ' />', $out); // <br /> not <br/> + $out = str_replace(' xmlns=""', '', $out); // rm unnecessary xmlns + + if (class_exists('Tidy')) { + // cleanup output + $config = array( + 'indent' => true, + 'output-xhtml' => true, + 'wrap' => 80 + ); + $tidy = new Tidy; + $tidy->parseString($out, $config, 'utf8'); + $tidy->cleanRepair(); + $out = (string) $tidy; + } + + return $out; + } + + /** + * Bulk sets parameters for the XSL stylesheet + * @param array $options Associative array of options to set + */ + public function setParameters($options) + { + foreach ($options as $name => $value) { + $this->xsltProcessor->setParameter('', $name, $value); + } + } + + /** + * Forward any other calls to the XSLT processor + */ + public function __call($name, $arguments) + { + call_user_func_array(array($this->xsltProcessor, $name), $arguments); + } + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/extras/FSTools.php b/vendor/ezyang/htmlpurifier/extras/FSTools.php new file mode 100644 index 000000000..ce0076316 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/extras/FSTools.php @@ -0,0 +1,164 @@ +<?php + +/** + * Filesystem tools not provided by default; can recursively create, copy + * and delete folders. Some template methods are provided for extensibility. + * + * @note This class must be instantiated to be used, although it does + * not maintain state. + */ +class FSTools +{ + + private static $singleton; + + /** + * Returns a global instance of FSTools + */ + public static function singleton() + { + if (empty(FSTools::$singleton)) FSTools::$singleton = new FSTools(); + return FSTools::$singleton; + } + + /** + * Sets our global singleton to something else; useful for overloading + * functions. + */ + public static function setSingleton($singleton) + { + FSTools::$singleton = $singleton; + } + + /** + * Recursively creates a directory + * @param string $folder Name of folder to create + * @note Adapted from the PHP manual comment 76612 + */ + public function mkdirr($folder) + { + $folders = preg_split("#[\\\\/]#", $folder); + $base = ''; + for($i = 0, $c = count($folders); $i < $c; $i++) { + if(empty($folders[$i])) { + if (!$i) { + // special case for root level + $base .= DIRECTORY_SEPARATOR; + } + continue; + } + $base .= $folders[$i]; + if(!is_dir($base)){ + $this->mkdir($base); + } + $base .= DIRECTORY_SEPARATOR; + } + } + + /** + * Copy a file, or recursively copy a folder and its contents; modified + * so that copied files, if PHP, have includes removed + * @note Adapted from http://aidanlister.com/repos/v/function.copyr.php + */ + public function copyr($source, $dest) + { + // Simple copy for a file + if (is_file($source)) { + return $this->copy($source, $dest); + } + // Make destination directory + if (!is_dir($dest)) { + $this->mkdir($dest); + } + // Loop through the folder + $dir = $this->dir($source); + while ( false !== ($entry = $dir->read()) ) { + // Skip pointers + if ($entry == '.' || $entry == '..') { + continue; + } + if (!$this->copyable($entry)) { + continue; + } + // Deep copy directories + if ($dest !== "$source/$entry") { + $this->copyr("$source/$entry", "$dest/$entry"); + } + } + // Clean up + $dir->close(); + return true; + } + + /** + * Overloadable function that tests a filename for copyability. By + * default, everything should be copied; you can restrict things to + * ignore hidden files, unreadable files, etc. This function + * applies to copyr(). + */ + public function copyable($file) + { + return true; + } + + /** + * Delete a file, or a folder and its contents + * @note Adapted from http://aidanlister.com/repos/v/function.rmdirr.php + */ + public function rmdirr($dirname) + { + // Sanity check + if (!$this->file_exists($dirname)) { + return false; + } + + // Simple delete for a file + if ($this->is_file($dirname) || $this->is_link($dirname)) { + return $this->unlink($dirname); + } + + // Loop through the folder + $dir = $this->dir($dirname); + while (false !== $entry = $dir->read()) { + // Skip pointers + if ($entry == '.' || $entry == '..') { + continue; + } + // Recurse + $this->rmdirr($dirname . DIRECTORY_SEPARATOR . $entry); + } + + // Clean up + $dir->close(); + return $this->rmdir($dirname); + } + + /** + * Recursively globs a directory. + */ + public function globr($dir, $pattern, $flags = NULL) + { + $files = $this->glob("$dir/$pattern", $flags); + if ($files === false) $files = array(); + $sub_dirs = $this->glob("$dir/*", GLOB_ONLYDIR); + if ($sub_dirs === false) $sub_dirs = array(); + foreach ($sub_dirs as $sub_dir) { + $sub_files = $this->globr($sub_dir, $pattern, $flags); + $files = array_merge($files, $sub_files); + } + return $files; + } + + /** + * Allows for PHP functions to be called and be stubbed. + * @warning This function will not work for functions that need + * to pass references; manually define a stub function for those. + */ + public function __call($name, $args) + { + return call_user_func_array($name, $args); + } + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/extras/FSTools/File.php b/vendor/ezyang/htmlpurifier/extras/FSTools/File.php new file mode 100644 index 000000000..6453a7a45 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/extras/FSTools/File.php @@ -0,0 +1,141 @@ +<?php + +/** + * Represents a file in the filesystem + * + * @warning Be sure to distinguish between get() and write() versus + * read() and put(), the former operates on the entire file, while + * the latter operates on a handle. + */ +class FSTools_File +{ + + /** Filename of file this object represents */ + protected $name; + + /** Handle for the file */ + protected $handle = false; + + /** Instance of FSTools for interfacing with filesystem */ + protected $fs; + + /** + * Filename of file you wish to instantiate. + * @note This file need not exist + */ + public function __construct($name, $fs = false) + { + $this->name = $name; + $this->fs = $fs ? $fs : FSTools::singleton(); + } + + /** Returns the filename of the file. */ + public function getName() {return $this->name;} + + /** Returns directory of the file without trailing slash */ + public function getDirectory() {return $this->fs->dirname($this->name);} + + /** + * Retrieves the contents of a file + * @todo Throw an exception if file doesn't exist + */ + public function get() + { + return $this->fs->file_get_contents($this->name); + } + + /** Writes contents to a file, creates new file if necessary */ + public function write($contents) + { + return $this->fs->file_put_contents($this->name, $contents); + } + + /** Deletes the file */ + public function delete() + { + return $this->fs->unlink($this->name); + } + + /** Returns true if file exists and is a file. */ + public function exists() + { + return $this->fs->is_file($this->name); + } + + /** Returns last file modification time */ + public function getMTime() + { + return $this->fs->filemtime($this->name); + } + + /** + * Chmod a file + * @note We ignore errors because of some weird owner trickery due + * to SVN duality + */ + public function chmod($octal_code) + { + return @$this->fs->chmod($this->name, $octal_code); + } + + /** Opens file's handle */ + public function open($mode) + { + if ($this->handle) $this->close(); + $this->handle = $this->fs->fopen($this->name, $mode); + return true; + } + + /** Closes file's handle */ + public function close() + { + if (!$this->handle) return false; + $status = $this->fs->fclose($this->handle); + $this->handle = false; + return $status; + } + + /** Retrieves a line from an open file, with optional max length $length */ + public function getLine($length = null) + { + if (!$this->handle) $this->open('r'); + if ($length === null) return $this->fs->fgets($this->handle); + else return $this->fs->fgets($this->handle, $length); + } + + /** Retrieves a character from an open file */ + public function getChar() + { + if (!$this->handle) $this->open('r'); + return $this->fs->fgetc($this->handle); + } + + /** Retrieves an $length bytes of data from an open data */ + public function read($length) + { + if (!$this->handle) $this->open('r'); + return $this->fs->fread($this->handle, $length); + } + + /** Writes to an open file */ + public function put($string) + { + if (!$this->handle) $this->open('a'); + return $this->fs->fwrite($this->handle, $string); + } + + /** Returns TRUE if the end of the file has been reached */ + public function eof() + { + if (!$this->handle) return true; + return $this->fs->feof($this->handle); + } + + public function __destruct() + { + if ($this->handle) $this->close(); + } + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/extras/HTMLPurifierExtras.auto.php b/vendor/ezyang/htmlpurifier/extras/HTMLPurifierExtras.auto.php new file mode 100644 index 000000000..4016d8afd --- /dev/null +++ b/vendor/ezyang/htmlpurifier/extras/HTMLPurifierExtras.auto.php @@ -0,0 +1,11 @@ +<?php + +/** + * This is a stub include that automatically configures the include path. + */ + +set_include_path(dirname(__FILE__) . PATH_SEPARATOR . get_include_path() ); +require_once 'HTMLPurifierExtras.php'; +require_once 'HTMLPurifierExtras.autoload.php'; + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/extras/HTMLPurifierExtras.autoload.php b/vendor/ezyang/htmlpurifier/extras/HTMLPurifierExtras.autoload.php new file mode 100644 index 000000000..de4a8aaaf --- /dev/null +++ b/vendor/ezyang/htmlpurifier/extras/HTMLPurifierExtras.autoload.php @@ -0,0 +1,26 @@ +<?php + +/** + * @file + * Convenience file that registers autoload handler for HTML Purifier. + * + * @warning + * This autoloader does not contain the compatibility code seen in + * HTMLPurifier_Bootstrap; the user is expected to make any necessary + * changes to use this library. + */ + +if (function_exists('spl_autoload_register')) { + spl_autoload_register(array('HTMLPurifierExtras', 'autoload')); + if (function_exists('__autoload')) { + // Be polite and ensure that userland autoload gets retained + spl_autoload_register('__autoload'); + } +} elseif (!function_exists('__autoload')) { + function __autoload($class) + { + return HTMLPurifierExtras::autoload($class); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/extras/HTMLPurifierExtras.php b/vendor/ezyang/htmlpurifier/extras/HTMLPurifierExtras.php new file mode 100644 index 000000000..35c2ca7e7 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/extras/HTMLPurifierExtras.php @@ -0,0 +1,31 @@ +<?php + +/** + * Meta-class for HTML Purifier's extra class hierarchies, similar to + * HTMLPurifier_Bootstrap. + */ +class HTMLPurifierExtras +{ + + public static function autoload($class) + { + $path = HTMLPurifierExtras::getPath($class); + if (!$path) return false; + require $path; + return true; + } + + public static function getPath($class) + { + if ( + strncmp('FSTools', $class, 7) !== 0 && + strncmp('ConfigDoc', $class, 9) !== 0 + ) return false; + // Custom implementations can go here + // Standard implementation: + return str_replace('_', '/', $class) . '.php'; + } + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/extras/README b/vendor/ezyang/htmlpurifier/extras/README new file mode 100644 index 000000000..4bfece79e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/extras/README @@ -0,0 +1,32 @@ + +HTML Purifier Extras + The Method Behind The Madness! + +The extras/ folder in HTML Purifier contains--you guessed it--extra things +for HTML Purifier. Specifically, these are two extra libraries called +FSTools and ConfigSchema. They're extra for a reason: you don't need them +if you're using HTML Purifier for normal usage: filtering HTML. However, +if you're a developer, and would like to test HTML Purifier, or need to +use one of HTML Purifier's maintenance scripts, chances are they'll need +these libraries. Who knows: maybe you'll find them useful too! + +Here are the libraries: + + +FSTools +------- + +Short for File System Tools, this is a poor-man's object-oriented wrapper for +the filesystem. It currently consists of two classes: + +- FSTools: This is a singleton that contains a manner of useful functions + such as recursive glob, directory removal, etc, as well as the ability + to call arbitrary native PHP functions through it like $FS->fopen(...). + This makes it a lot simpler to mock these filesystem calls for unit testing. + +- FSTools_File: This object represents a single file, and has almost any + method imaginable one would need. + +Check the files themselves for more information. + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier.auto.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.auto.php new file mode 100644 index 000000000..1960c399f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.auto.php @@ -0,0 +1,11 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier.autoload.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.autoload.php new file mode 100644 index 000000000..c3ea67e81 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.autoload.php @@ -0,0 +1,27 @@ +<?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')) { + function __autoload($class) + { + return HTMLPurifier_Bootstrap::autoload($class); + } +} + +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier.composer.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.composer.php new file mode 100644 index 000000000..52acc56b0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.composer.php @@ -0,0 +1,4 @@ +<?php +if (!defined('HTMLPURIFIER_PREFIX')) { + define('HTMLPURIFIER_PREFIX', dirname(__FILE__)); +} diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier.func.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.func.php new file mode 100644 index 000000000..64b140bec --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.func.php @@ -0,0 +1,25 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier.includes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.includes.php new file mode 100644 index 000000000..7779fe34d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.includes.php @@ -0,0 +1,234 @@ +<?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.9.2 + * + * @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/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'; diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier.kses.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.kses.php new file mode 100644 index 000000000..752290077 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.kses.php @@ -0,0 +1,30 @@ +<?php + +/** + * @file + * Emulation layer for code that used kses(), substituting in HTML Purifier. + */ + +require_once dirname(__FILE__) . '/HTMLPurifier.auto.php'; + +function kses($string, $allowed_html, $allowed_protocols = null) +{ + $config = HTMLPurifier_Config::createDefault(); + $allowed_elements = array(); + $allowed_attributes = array(); + foreach ($allowed_html as $element => $attributes) { + $allowed_elements[$element] = true; + foreach ($attributes as $attribute => $x) { + $allowed_attributes["$element.$attribute"] = true; + } + } + $config->set('HTML.AllowedElements', $allowed_elements); + $config->set('HTML.AllowedAttributes', $allowed_attributes); + if ($allowed_protocols !== null) { + $config->set('URI.AllowedSchemes', $allowed_protocols); + } + $purifier = new HTMLPurifier($config); + return $purifier->purify($string); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier.path.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.path.php new file mode 100644 index 000000000..39b1b6531 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.path.php @@ -0,0 +1,11 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.php new file mode 100644 index 000000000..9c539e225 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.php @@ -0,0 +1,292 @@ +<?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.9.2 - 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.9.2'; + + /** + * Constant with version of HTML Purifier. + */ + const VERSION = '4.9.2'; + + /** + * 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(); + foreach ($array_of_html as $key => $html) { + $array_of_html[$key] = $this->purify($html, $config); + $context_array[$key] = $this->context; + } + $this->context = $context_array; + return $array_of_html; + } + + /** + * 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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier.safe-includes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.safe-includes.php new file mode 100644 index 000000000..a3261f8a3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier.safe-includes.php @@ -0,0 +1,228 @@ +<?php + +/** + * @file + * This file was auto-generated by generate-includes.php and includes all of + * the core files required by HTML Purifier. This is a convenience stub that + * includes all files using dirname(__FILE__) and require_once. PLEASE DO NOT + * EDIT THIS FILE, changes will be overwritten the next time the script is run. + * + * Changes to include_path are not necessary. + */ + +$__dir = dirname(__FILE__); + +require_once $__dir . '/HTMLPurifier.php'; +require_once $__dir . '/HTMLPurifier/Arborize.php'; +require_once $__dir . '/HTMLPurifier/AttrCollections.php'; +require_once $__dir . '/HTMLPurifier/AttrDef.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform.php'; +require_once $__dir . '/HTMLPurifier/AttrTypes.php'; +require_once $__dir . '/HTMLPurifier/AttrValidator.php'; +require_once $__dir . '/HTMLPurifier/Bootstrap.php'; +require_once $__dir . '/HTMLPurifier/Definition.php'; +require_once $__dir . '/HTMLPurifier/CSSDefinition.php'; +require_once $__dir . '/HTMLPurifier/ChildDef.php'; +require_once $__dir . '/HTMLPurifier/Config.php'; +require_once $__dir . '/HTMLPurifier/ConfigSchema.php'; +require_once $__dir . '/HTMLPurifier/ContentSets.php'; +require_once $__dir . '/HTMLPurifier/Context.php'; +require_once $__dir . '/HTMLPurifier/DefinitionCache.php'; +require_once $__dir . '/HTMLPurifier/DefinitionCacheFactory.php'; +require_once $__dir . '/HTMLPurifier/Doctype.php'; +require_once $__dir . '/HTMLPurifier/DoctypeRegistry.php'; +require_once $__dir . '/HTMLPurifier/ElementDef.php'; +require_once $__dir . '/HTMLPurifier/Encoder.php'; +require_once $__dir . '/HTMLPurifier/EntityLookup.php'; +require_once $__dir . '/HTMLPurifier/EntityParser.php'; +require_once $__dir . '/HTMLPurifier/ErrorCollector.php'; +require_once $__dir . '/HTMLPurifier/ErrorStruct.php'; +require_once $__dir . '/HTMLPurifier/Exception.php'; +require_once $__dir . '/HTMLPurifier/Filter.php'; +require_once $__dir . '/HTMLPurifier/Generator.php'; +require_once $__dir . '/HTMLPurifier/HTMLDefinition.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule.php'; +require_once $__dir . '/HTMLPurifier/HTMLModuleManager.php'; +require_once $__dir . '/HTMLPurifier/IDAccumulator.php'; +require_once $__dir . '/HTMLPurifier/Injector.php'; +require_once $__dir . '/HTMLPurifier/Language.php'; +require_once $__dir . '/HTMLPurifier/LanguageFactory.php'; +require_once $__dir . '/HTMLPurifier/Length.php'; +require_once $__dir . '/HTMLPurifier/Lexer.php'; +require_once $__dir . '/HTMLPurifier/Node.php'; +require_once $__dir . '/HTMLPurifier/PercentEncoder.php'; +require_once $__dir . '/HTMLPurifier/PropertyList.php'; +require_once $__dir . '/HTMLPurifier/PropertyListIterator.php'; +require_once $__dir . '/HTMLPurifier/Queue.php'; +require_once $__dir . '/HTMLPurifier/Strategy.php'; +require_once $__dir . '/HTMLPurifier/StringHash.php'; +require_once $__dir . '/HTMLPurifier/StringHashParser.php'; +require_once $__dir . '/HTMLPurifier/TagTransform.php'; +require_once $__dir . '/HTMLPurifier/Token.php'; +require_once $__dir . '/HTMLPurifier/TokenFactory.php'; +require_once $__dir . '/HTMLPurifier/URI.php'; +require_once $__dir . '/HTMLPurifier/URIDefinition.php'; +require_once $__dir . '/HTMLPurifier/URIFilter.php'; +require_once $__dir . '/HTMLPurifier/URIParser.php'; +require_once $__dir . '/HTMLPurifier/URIScheme.php'; +require_once $__dir . '/HTMLPurifier/URISchemeRegistry.php'; +require_once $__dir . '/HTMLPurifier/UnitConverter.php'; +require_once $__dir . '/HTMLPurifier/VarParser.php'; +require_once $__dir . '/HTMLPurifier/VarParserException.php'; +require_once $__dir . '/HTMLPurifier/Zipper.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/Clone.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/Enum.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/Integer.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/Lang.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/Switch.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/Text.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/URI.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Number.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/AlphaValue.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Background.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Border.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Color.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Composite.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Filter.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Font.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/FontFamily.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Ident.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/ImportantDecorator.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Length.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/ListStyle.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Multiple.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Percentage.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/TextDecoration.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/CSS/URI.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/HTML/Bool.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/HTML/Nmtokens.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/HTML/Class.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/HTML/Color.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/HTML/FrameTarget.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/HTML/ID.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/HTML/Pixels.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/HTML/Length.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/HTML/LinkTypes.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/HTML/MultiLength.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/URI/Email.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/URI/Host.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/URI/IPv4.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/URI/IPv6.php'; +require_once $__dir . '/HTMLPurifier/AttrDef/URI/Email/SimpleCheck.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/Background.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/BdoDir.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/BgColor.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/BoolToCSS.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/Border.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/EnumToCSS.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/ImgRequired.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/ImgSpace.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/Input.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/Lang.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/Length.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/Name.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/NameSync.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/Nofollow.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/SafeEmbed.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/SafeObject.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/SafeParam.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/ScriptRequired.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/TargetBlank.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/TargetNoopener.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/TargetNoreferrer.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/Textarea.php'; +require_once $__dir . '/HTMLPurifier/ChildDef/Chameleon.php'; +require_once $__dir . '/HTMLPurifier/ChildDef/Custom.php'; +require_once $__dir . '/HTMLPurifier/ChildDef/Empty.php'; +require_once $__dir . '/HTMLPurifier/ChildDef/List.php'; +require_once $__dir . '/HTMLPurifier/ChildDef/Required.php'; +require_once $__dir . '/HTMLPurifier/ChildDef/Optional.php'; +require_once $__dir . '/HTMLPurifier/ChildDef/StrictBlockquote.php'; +require_once $__dir . '/HTMLPurifier/ChildDef/Table.php'; +require_once $__dir . '/HTMLPurifier/DefinitionCache/Decorator.php'; +require_once $__dir . '/HTMLPurifier/DefinitionCache/Null.php'; +require_once $__dir . '/HTMLPurifier/DefinitionCache/Serializer.php'; +require_once $__dir . '/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php'; +require_once $__dir . '/HTMLPurifier/DefinitionCache/Decorator/Memory.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Bdo.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/CommonAttributes.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Edit.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Forms.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Hypertext.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Iframe.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Image.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Legacy.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/List.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Name.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Nofollow.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Object.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Presentation.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Proprietary.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Ruby.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/SafeEmbed.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/SafeObject.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/SafeScripting.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Scripting.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/StyleAttribute.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Tables.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Target.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/TargetBlank.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/TargetNoopener.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/TargetNoreferrer.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Text.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Tidy.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/XMLCommonAttributes.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Tidy/Name.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Tidy/Proprietary.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Tidy/Strict.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Tidy/Transitional.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/Tidy/XHTML.php'; +require_once $__dir . '/HTMLPurifier/Injector/AutoParagraph.php'; +require_once $__dir . '/HTMLPurifier/Injector/DisplayLinkURI.php'; +require_once $__dir . '/HTMLPurifier/Injector/Linkify.php'; +require_once $__dir . '/HTMLPurifier/Injector/PurifierLinkify.php'; +require_once $__dir . '/HTMLPurifier/Injector/RemoveEmpty.php'; +require_once $__dir . '/HTMLPurifier/Injector/RemoveSpansWithoutAttributes.php'; +require_once $__dir . '/HTMLPurifier/Injector/SafeObject.php'; +require_once $__dir . '/HTMLPurifier/Lexer/DOMLex.php'; +require_once $__dir . '/HTMLPurifier/Lexer/DirectLex.php'; +require_once $__dir . '/HTMLPurifier/Node/Comment.php'; +require_once $__dir . '/HTMLPurifier/Node/Element.php'; +require_once $__dir . '/HTMLPurifier/Node/Text.php'; +require_once $__dir . '/HTMLPurifier/Strategy/Composite.php'; +require_once $__dir . '/HTMLPurifier/Strategy/Core.php'; +require_once $__dir . '/HTMLPurifier/Strategy/FixNesting.php'; +require_once $__dir . '/HTMLPurifier/Strategy/MakeWellFormed.php'; +require_once $__dir . '/HTMLPurifier/Strategy/RemoveForeignElements.php'; +require_once $__dir . '/HTMLPurifier/Strategy/ValidateAttributes.php'; +require_once $__dir . '/HTMLPurifier/TagTransform/Font.php'; +require_once $__dir . '/HTMLPurifier/TagTransform/Simple.php'; +require_once $__dir . '/HTMLPurifier/Token/Comment.php'; +require_once $__dir . '/HTMLPurifier/Token/Tag.php'; +require_once $__dir . '/HTMLPurifier/Token/Empty.php'; +require_once $__dir . '/HTMLPurifier/Token/End.php'; +require_once $__dir . '/HTMLPurifier/Token/Start.php'; +require_once $__dir . '/HTMLPurifier/Token/Text.php'; +require_once $__dir . '/HTMLPurifier/URIFilter/DisableExternal.php'; +require_once $__dir . '/HTMLPurifier/URIFilter/DisableExternalResources.php'; +require_once $__dir . '/HTMLPurifier/URIFilter/DisableResources.php'; +require_once $__dir . '/HTMLPurifier/URIFilter/HostBlacklist.php'; +require_once $__dir . '/HTMLPurifier/URIFilter/MakeAbsolute.php'; +require_once $__dir . '/HTMLPurifier/URIFilter/Munge.php'; +require_once $__dir . '/HTMLPurifier/URIFilter/SafeIframe.php'; +require_once $__dir . '/HTMLPurifier/URIScheme/data.php'; +require_once $__dir . '/HTMLPurifier/URIScheme/file.php'; +require_once $__dir . '/HTMLPurifier/URIScheme/ftp.php'; +require_once $__dir . '/HTMLPurifier/URIScheme/http.php'; +require_once $__dir . '/HTMLPurifier/URIScheme/https.php'; +require_once $__dir . '/HTMLPurifier/URIScheme/mailto.php'; +require_once $__dir . '/HTMLPurifier/URIScheme/news.php'; +require_once $__dir . '/HTMLPurifier/URIScheme/nntp.php'; +require_once $__dir . '/HTMLPurifier/URIScheme/tel.php'; +require_once $__dir . '/HTMLPurifier/VarParser/Flexible.php'; +require_once $__dir . '/HTMLPurifier/VarParser/Native.php'; diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Arborize.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Arborize.php new file mode 100644 index 000000000..d2e9d22a2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Arborize.php @@ -0,0 +1,71 @@ +<?php + +/** + * Converts a stream of HTMLPurifier_Token into an HTMLPurifier_Node, + * and back again. + * + * @note This transformation is not an equivalence. We mutate the input + * token stream to make it so; see all [MUT] markers in code. + */ +class HTMLPurifier_Arborize +{ + public static function arborize($tokens, $config, $context) { + $definition = $config->getHTMLDefinition(); + $parent = new HTMLPurifier_Token_Start($definition->info_parent); + $stack = array($parent->toNode()); + foreach ($tokens as $token) { + $token->skip = null; // [MUT] + $token->carryover = null; // [MUT] + if ($token instanceof HTMLPurifier_Token_End) { + $token->start = null; // [MUT] + $r = array_pop($stack); + //assert($r->name === $token->name); + //assert(empty($token->attr)); + $r->endCol = $token->col; + $r->endLine = $token->line; + $r->endArmor = $token->armor; + continue; + } + $node = $token->toNode(); + $stack[count($stack)-1]->children[] = $node; + if ($token instanceof HTMLPurifier_Token_Start) { + $stack[] = $node; + } + } + //assert(count($stack) == 1); + return $stack[0]; + } + + public static function flatten($node, $config, $context) { + $level = 0; + $nodes = array($level => new HTMLPurifier_Queue(array($node))); + $closingTokens = array(); + $tokens = array(); + do { + while (!$nodes[$level]->isEmpty()) { + $node = $nodes[$level]->shift(); // FIFO + list($start, $end) = $node->toTokenPair(); + if ($level > 0) { + $tokens[] = $start; + } + if ($end !== NULL) { + $closingTokens[$level][] = $end; + } + if ($node instanceof HTMLPurifier_Node_Element) { + $level++; + $nodes[$level] = new HTMLPurifier_Queue(); + foreach ($node->children as $childNode) { + $nodes[$level]->push($childNode); + } + } + } + $level--; + if ($level && isset($closingTokens[$level])) { + while ($token = array_pop($closingTokens[$level])) { + $tokens[] = $token; + } + } + } while ($level > 0); + return $tokens; + } +} diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrCollections.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrCollections.php new file mode 100644 index 000000000..c7b17cf14 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrCollections.php @@ -0,0 +1,148 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef.php new file mode 100644 index 000000000..739646fa7 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef.php @@ -0,0 +1,144 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS.php new file mode 100644 index 000000000..ad2cb90ad --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS.php @@ -0,0 +1,136 @@ +<?php + +/** + * Validates the HTML attribute style, otherwise known as CSS. + * @note We don't implement the whole CSS specification, so it might be + * difficult to reuse this component in the context of validating + * actual stylesheet declarations. + * @note If we were really serious about validating the CSS, we would + * tokenize the styles and then parse the tokens. Obviously, we + * are not doing that. Doing that could seriously harm performance, + * but would make these components a lot more viable for a CSS + * filtering solution. + */ +class HTMLPurifier_AttrDef_CSS extends HTMLPurifier_AttrDef +{ + + /** + * @param string $css + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($css, $config, $context) + { + $css = $this->parseCDATA($css); + + $definition = $config->getCSSDefinition(); + $allow_duplicates = $config->get("CSS.AllowDuplicates"); + + + // According to the CSS2.1 spec, the places where a + // non-delimiting semicolon can appear are in strings + // escape sequences. So here is some dumb hack to + // handle quotes. + $len = strlen($css); + $accum = ""; + $declarations = array(); + $quoted = false; + for ($i = 0; $i < $len; $i++) { + $c = strcspn($css, ";'\"", $i); + $accum .= substr($css, $i, $c); + $i += $c; + if ($i == $len) break; + $d = $css[$i]; + if ($quoted) { + $accum .= $d; + if ($d == $quoted) { + $quoted = false; + } + } else { + if ($d == ";") { + $declarations[] = $accum; + $accum = ""; + } else { + $accum .= $d; + $quoted = $d; + } + } + } + if ($accum != "") $declarations[] = $accum; + + $propvalues = array(); + $new_declarations = ''; + + /** + * Name of the current CSS property being validated. + */ + $property = false; + $context->register('CurrentCSSProperty', $property); + + foreach ($declarations as $declaration) { + if (!$declaration) { + continue; + } + if (!strpos($declaration, ':')) { + continue; + } + list($property, $value) = explode(':', $declaration, 2); + $property = trim($property); + $value = trim($value); + $ok = false; + do { + if (isset($definition->info[$property])) { + $ok = true; + break; + } + if (ctype_lower($property)) { + break; + } + $property = strtolower($property); + if (isset($definition->info[$property])) { + $ok = true; + break; + } + } while (0); + if (!$ok) { + continue; + } + // inefficient call, since the validator will do this again + if (strtolower(trim($value)) !== 'inherit') { + // inherit works for everything (but only on the base property) + $result = $definition->info[$property]->validate( + $value, + $config, + $context + ); + } else { + $result = 'inherit'; + } + if ($result === false) { + continue; + } + if ($allow_duplicates) { + $new_declarations .= "$property:$result;"; + } else { + $propvalues[$property] = $result; + } + } + + $context->destroy('CurrentCSSProperty'); + + // procedure does not write the new CSS simultaneously, so it's + // slightly inefficient, but it's the only way of getting rid of + // duplicates. Perhaps config to optimize it, but not now. + + foreach ($propvalues as $prop => $value) { + $new_declarations .= "$prop:$value;"; + } + + return $new_declarations ? $new_declarations : false; + + } + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/AlphaValue.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/AlphaValue.php new file mode 100644 index 000000000..af2b83dff --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/AlphaValue.php @@ -0,0 +1,34 @@ +<?php + +class HTMLPurifier_AttrDef_CSS_AlphaValue extends HTMLPurifier_AttrDef_CSS_Number +{ + + public function __construct() + { + parent::__construct(false); // opacity is non-negative, but we will clamp it + } + + /** + * @param string $number + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function validate($number, $config, $context) + { + $result = parent::validate($number, $config, $context); + if ($result === false) { + return $result; + } + $float = (float)$result; + if ($float < 0.0) { + $result = '0'; + } + if ($float > 1.0) { + $result = '1'; + } + return $result; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Background.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Background.php new file mode 100644 index 000000000..7f1ea3b0f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Background.php @@ -0,0 +1,111 @@ +<?php + +/** + * Validates shorthand CSS property background. + * @warning Does not support url tokens that have internal spaces. + */ +class HTMLPurifier_AttrDef_CSS_Background extends HTMLPurifier_AttrDef +{ + + /** + * Local copy of component validators. + * @type HTMLPurifier_AttrDef[] + * @note See HTMLPurifier_AttrDef_Font::$info for a similar impl. + */ + protected $info; + + /** + * @param HTMLPurifier_Config $config + */ + public function __construct($config) + { + $def = $config->getCSSDefinition(); + $this->info['background-color'] = $def->info['background-color']; + $this->info['background-image'] = $def->info['background-image']; + $this->info['background-repeat'] = $def->info['background-repeat']; + $this->info['background-attachment'] = $def->info['background-attachment']; + $this->info['background-position'] = $def->info['background-position']; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + // regular pre-processing + $string = $this->parseCDATA($string); + if ($string === '') { + return false; + } + + // munge rgb() decl if necessary + $string = $this->mungeRgb($string); + + // assumes URI doesn't have spaces in it + $bits = explode(' ', $string); // bits to process + + $caught = array(); + $caught['color'] = false; + $caught['image'] = false; + $caught['repeat'] = false; + $caught['attachment'] = false; + $caught['position'] = false; + + $i = 0; // number of catches + + foreach ($bits as $bit) { + if ($bit === '') { + continue; + } + foreach ($caught as $key => $status) { + if ($key != 'position') { + if ($status !== false) { + continue; + } + $r = $this->info['background-' . $key]->validate($bit, $config, $context); + } else { + $r = $bit; + } + if ($r === false) { + continue; + } + if ($key == 'position') { + if ($caught[$key] === false) { + $caught[$key] = ''; + } + $caught[$key] .= $r . ' '; + } else { + $caught[$key] = $r; + } + $i++; + break; + } + } + + if (!$i) { + return false; + } + if ($caught['position'] !== false) { + $caught['position'] = $this->info['background-position']-> + validate($caught['position'], $config, $context); + } + + $ret = array(); + foreach ($caught as $value) { + if ($value === false) { + continue; + } + $ret[] = $value; + } + + if (empty($ret)) { + return false; + } + return implode(' ', $ret); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php new file mode 100644 index 000000000..4580ef5a9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php @@ -0,0 +1,157 @@ +<?php + +/* W3C says: + [ // adjective and number must be in correct order, even if + // you could switch them without introducing ambiguity. + // some browsers support that syntax + [ + <percentage> | <length> | left | center | right + ] + [ + <percentage> | <length> | top | center | bottom + ]? + ] | + [ // this signifies that the vertical and horizontal adjectives + // can be arbitrarily ordered, however, there can only be two, + // one of each, or none at all + [ + left | center | right + ] || + [ + top | center | bottom + ] + ] + top, left = 0% + center, (none) = 50% + bottom, right = 100% +*/ + +/* QuirksMode says: + keyword + length/percentage must be ordered correctly, as per W3C + + Internet Explorer and Opera, however, support arbitrary ordering. We + should fix it up. + + Minor issue though, not strictly necessary. +*/ + +// control freaks may appreciate the ability to convert these to +// percentages or something, but it's not necessary + +/** + * Validates the value of background-position. + */ +class HTMLPurifier_AttrDef_CSS_BackgroundPosition extends HTMLPurifier_AttrDef +{ + + /** + * @type HTMLPurifier_AttrDef_CSS_Length + */ + protected $length; + + /** + * @type HTMLPurifier_AttrDef_CSS_Percentage + */ + protected $percentage; + + public function __construct() + { + $this->length = new HTMLPurifier_AttrDef_CSS_Length(); + $this->percentage = new HTMLPurifier_AttrDef_CSS_Percentage(); + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = $this->parseCDATA($string); + $bits = explode(' ', $string); + + $keywords = array(); + $keywords['h'] = false; // left, right + $keywords['v'] = false; // top, bottom + $keywords['ch'] = false; // center (first word) + $keywords['cv'] = false; // center (second word) + $measures = array(); + + $i = 0; + + $lookup = array( + 'top' => 'v', + 'bottom' => 'v', + 'left' => 'h', + 'right' => 'h', + 'center' => 'c' + ); + + foreach ($bits as $bit) { + if ($bit === '') { + continue; + } + + // test for keyword + $lbit = ctype_lower($bit) ? $bit : strtolower($bit); + if (isset($lookup[$lbit])) { + $status = $lookup[$lbit]; + if ($status == 'c') { + if ($i == 0) { + $status = 'ch'; + } else { + $status = 'cv'; + } + } + $keywords[$status] = $lbit; + $i++; + } + + // test for length + $r = $this->length->validate($bit, $config, $context); + if ($r !== false) { + $measures[] = $r; + $i++; + } + + // test for percentage + $r = $this->percentage->validate($bit, $config, $context); + if ($r !== false) { + $measures[] = $r; + $i++; + } + } + + if (!$i) { + return false; + } // no valid values were caught + + $ret = array(); + + // first keyword + if ($keywords['h']) { + $ret[] = $keywords['h']; + } elseif ($keywords['ch']) { + $ret[] = $keywords['ch']; + $keywords['cv'] = false; // prevent re-use: center = center center + } elseif (count($measures)) { + $ret[] = array_shift($measures); + } + + if ($keywords['v']) { + $ret[] = $keywords['v']; + } elseif ($keywords['cv']) { + $ret[] = $keywords['cv']; + } elseif (count($measures)) { + $ret[] = array_shift($measures); + } + + if (empty($ret)) { + return false; + } + return implode(' ', $ret); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Border.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Border.php new file mode 100644 index 000000000..16243ba1e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Border.php @@ -0,0 +1,56 @@ +<?php + +/** + * Validates the border property as defined by CSS. + */ +class HTMLPurifier_AttrDef_CSS_Border extends HTMLPurifier_AttrDef +{ + + /** + * Local copy of properties this property is shorthand for. + * @type HTMLPurifier_AttrDef[] + */ + protected $info = array(); + + /** + * @param HTMLPurifier_Config $config + */ + public function __construct($config) + { + $def = $config->getCSSDefinition(); + $this->info['border-width'] = $def->info['border-width']; + $this->info['border-style'] = $def->info['border-style']; + $this->info['border-top-color'] = $def->info['border-top-color']; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = $this->parseCDATA($string); + $string = $this->mungeRgb($string); + $bits = explode(' ', $string); + $done = array(); // segments we've finished + $ret = ''; // return value + foreach ($bits as $bit) { + foreach ($this->info as $propname => $validator) { + if (isset($done[$propname])) { + continue; + } + $r = $validator->validate($bit, $config, $context); + if ($r !== false) { + $ret .= $r . ' '; + $done[$propname] = true; + break; + } + } + } + return rtrim($ret); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Color.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Color.php new file mode 100644 index 000000000..d7287a00c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Color.php @@ -0,0 +1,161 @@ +<?php + +/** + * Validates Color as defined by CSS. + */ +class HTMLPurifier_AttrDef_CSS_Color extends HTMLPurifier_AttrDef +{ + + /** + * @type HTMLPurifier_AttrDef_CSS_AlphaValue + */ + protected $alpha; + + public function __construct() + { + $this->alpha = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + } + + /** + * @param string $color + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($color, $config, $context) + { + static $colors = null; + if ($colors === null) { + $colors = $config->get('Core.ColorKeywords'); + } + + $color = trim($color); + if ($color === '') { + return false; + } + + $lower = strtolower($color); + if (isset($colors[$lower])) { + return $colors[$lower]; + } + + if (preg_match('#(rgb|rgba|hsl|hsla)\(#', $color, $matches) === 1) { + $length = strlen($color); + if (strpos($color, ')') !== $length - 1) { + return false; + } + + // get used function : rgb, rgba, hsl or hsla + $function = $matches[1]; + + $parameters_size = 3; + $alpha_channel = false; + if (substr($function, -1) === 'a') { + $parameters_size = 4; + $alpha_channel = true; + } + + /* + * Allowed types for values : + * parameter_position => [type => max_value] + */ + $allowed_types = array( + 1 => array('percentage' => 100, 'integer' => 255), + 2 => array('percentage' => 100, 'integer' => 255), + 3 => array('percentage' => 100, 'integer' => 255), + ); + $allow_different_types = false; + + if (strpos($function, 'hsl') !== false) { + $allowed_types = array( + 1 => array('integer' => 360), + 2 => array('percentage' => 100), + 3 => array('percentage' => 100), + ); + $allow_different_types = true; + } + + $values = trim(str_replace($function, '', $color), ' ()'); + + $parts = explode(',', $values); + if (count($parts) !== $parameters_size) { + return false; + } + + $type = false; + $new_parts = array(); + $i = 0; + + foreach ($parts as $part) { + $i++; + $part = trim($part); + + if ($part === '') { + return false; + } + + // different check for alpha channel + if ($alpha_channel === true && $i === count($parts)) { + $result = $this->alpha->validate($part, $config, $context); + + if ($result === false) { + return false; + } + + $new_parts[] = (string)$result; + continue; + } + + if (substr($part, -1) === '%') { + $current_type = 'percentage'; + } else { + $current_type = 'integer'; + } + + if (!array_key_exists($current_type, $allowed_types[$i])) { + return false; + } + + if (!$type) { + $type = $current_type; + } + + if ($allow_different_types === false && $type != $current_type) { + return false; + } + + $max_value = $allowed_types[$i][$current_type]; + + if ($current_type == 'integer') { + // Return value between range 0 -> $max_value + $new_parts[] = (int)max(min($part, $max_value), 0); + } elseif ($current_type == 'percentage') { + $new_parts[] = (float)max(min(rtrim($part, '%'), $max_value), 0) . '%'; + } + } + + $new_values = implode(',', $new_parts); + + $color = $function . '(' . $new_values . ')'; + } else { + // hexadecimal handling + if ($color[0] === '#') { + $hex = substr($color, 1); + } else { + $hex = $color; + $color = '#' . $color; + } + $length = strlen($hex); + if ($length !== 3 && $length !== 6) { + return false; + } + if (!ctype_xdigit($hex)) { + return false; + } + } + return $color; + } + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Composite.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Composite.php new file mode 100644 index 000000000..9c1750554 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Composite.php @@ -0,0 +1,48 @@ +<?php + +/** + * Allows multiple validators to attempt to validate attribute. + * + * Composite is just what it sounds like: a composite of many validators. + * This means that multiple HTMLPurifier_AttrDef objects will have a whack + * at the string. If one of them passes, that's what is returned. This is + * especially useful for CSS values, which often are a choice between + * an enumerated set of predefined values or a flexible data type. + */ +class HTMLPurifier_AttrDef_CSS_Composite extends HTMLPurifier_AttrDef +{ + + /** + * List of objects that may process strings. + * @type HTMLPurifier_AttrDef[] + * @todo Make protected + */ + public $defs; + + /** + * @param HTMLPurifier_AttrDef[] $defs List of HTMLPurifier_AttrDef objects + */ + public function __construct($defs) + { + $this->defs = $defs; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + foreach ($this->defs as $i => $def) { + $result = $this->defs[$i]->validate($string, $config, $context); + if ($result !== false) { + return $result; + } + } + return false; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php new file mode 100644 index 000000000..9d77cc9aa --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php @@ -0,0 +1,44 @@ +<?php + +/** + * Decorator which enables CSS properties to be disabled for specific elements. + */ +class HTMLPurifier_AttrDef_CSS_DenyElementDecorator extends HTMLPurifier_AttrDef +{ + /** + * @type HTMLPurifier_AttrDef + */ + public $def; + /** + * @type string + */ + public $element; + + /** + * @param HTMLPurifier_AttrDef $def Definition to wrap + * @param string $element Element to deny + */ + public function __construct($def, $element) + { + $this->def = $def; + $this->element = $element; + } + + /** + * Checks if CurrentToken is set and equal to $this->element + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $token = $context->get('CurrentToken', true); + if ($token && $token->name == $this->element) { + return false; + } + return $this->def->validate($string, $config, $context); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Filter.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Filter.php new file mode 100644 index 000000000..bde4c3301 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Filter.php @@ -0,0 +1,77 @@ +<?php + +/** + * Microsoft's proprietary filter: CSS property + * @note Currently supports the alpha filter. In the future, this will + * probably need an extensible framework + */ +class HTMLPurifier_AttrDef_CSS_Filter extends HTMLPurifier_AttrDef +{ + /** + * @type HTMLPurifier_AttrDef_Integer + */ + protected $intValidator; + + public function __construct() + { + $this->intValidator = new HTMLPurifier_AttrDef_Integer(); + } + + /** + * @param string $value + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($value, $config, $context) + { + $value = $this->parseCDATA($value); + if ($value === 'none') { + return $value; + } + // if we looped this we could support multiple filters + $function_length = strcspn($value, '('); + $function = trim(substr($value, 0, $function_length)); + if ($function !== 'alpha' && + $function !== 'Alpha' && + $function !== 'progid:DXImageTransform.Microsoft.Alpha' + ) { + return false; + } + $cursor = $function_length + 1; + $parameters_length = strcspn($value, ')', $cursor); + $parameters = substr($value, $cursor, $parameters_length); + $params = explode(',', $parameters); + $ret_params = array(); + $lookup = array(); + foreach ($params as $param) { + list($key, $value) = explode('=', $param); + $key = trim($key); + $value = trim($value); + if (isset($lookup[$key])) { + continue; + } + if ($key !== 'opacity') { + continue; + } + $value = $this->intValidator->validate($value, $config, $context); + if ($value === false) { + continue; + } + $int = (int)$value; + if ($int > 100) { + $value = '100'; + } + if ($int < 0) { + $value = '0'; + } + $ret_params[] = "$key=$value"; + $lookup[$key] = true; + } + $ret_parameters = implode(',', $ret_params); + $ret_function = "$function($ret_parameters)"; + return $ret_function; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Font.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Font.php new file mode 100644 index 000000000..579b97ef1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Font.php @@ -0,0 +1,176 @@ +<?php + +/** + * Validates shorthand CSS property font. + */ +class HTMLPurifier_AttrDef_CSS_Font extends HTMLPurifier_AttrDef +{ + + /** + * Local copy of validators + * @type HTMLPurifier_AttrDef[] + * @note If we moved specific CSS property definitions to their own + * classes instead of having them be assembled at run time by + * CSSDefinition, this wouldn't be necessary. We'd instantiate + * our own copies. + */ + protected $info = array(); + + /** + * @param HTMLPurifier_Config $config + */ + public function __construct($config) + { + $def = $config->getCSSDefinition(); + $this->info['font-style'] = $def->info['font-style']; + $this->info['font-variant'] = $def->info['font-variant']; + $this->info['font-weight'] = $def->info['font-weight']; + $this->info['font-size'] = $def->info['font-size']; + $this->info['line-height'] = $def->info['line-height']; + $this->info['font-family'] = $def->info['font-family']; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + static $system_fonts = array( + 'caption' => true, + 'icon' => true, + 'menu' => true, + 'message-box' => true, + 'small-caption' => true, + 'status-bar' => true + ); + + // regular pre-processing + $string = $this->parseCDATA($string); + if ($string === '') { + return false; + } + + // check if it's one of the keywords + $lowercase_string = strtolower($string); + if (isset($system_fonts[$lowercase_string])) { + return $lowercase_string; + } + + $bits = explode(' ', $string); // bits to process + $stage = 0; // this indicates what we're looking for + $caught = array(); // which stage 0 properties have we caught? + $stage_1 = array('font-style', 'font-variant', 'font-weight'); + $final = ''; // output + + for ($i = 0, $size = count($bits); $i < $size; $i++) { + if ($bits[$i] === '') { + continue; + } + switch ($stage) { + case 0: // attempting to catch font-style, font-variant or font-weight + foreach ($stage_1 as $validator_name) { + if (isset($caught[$validator_name])) { + continue; + } + $r = $this->info[$validator_name]->validate( + $bits[$i], + $config, + $context + ); + if ($r !== false) { + $final .= $r . ' '; + $caught[$validator_name] = true; + break; + } + } + // all three caught, continue on + if (count($caught) >= 3) { + $stage = 1; + } + if ($r !== false) { + break; + } + case 1: // attempting to catch font-size and perhaps line-height + $found_slash = false; + if (strpos($bits[$i], '/') !== false) { + list($font_size, $line_height) = + explode('/', $bits[$i]); + if ($line_height === '') { + // ooh, there's a space after the slash! + $line_height = false; + $found_slash = true; + } + } else { + $font_size = $bits[$i]; + $line_height = false; + } + $r = $this->info['font-size']->validate( + $font_size, + $config, + $context + ); + if ($r !== false) { + $final .= $r; + // attempt to catch line-height + if ($line_height === false) { + // we need to scroll forward + for ($j = $i + 1; $j < $size; $j++) { + if ($bits[$j] === '') { + continue; + } + if ($bits[$j] === '/') { + if ($found_slash) { + return false; + } else { + $found_slash = true; + continue; + } + } + $line_height = $bits[$j]; + break; + } + } else { + // slash already found + $found_slash = true; + $j = $i; + } + if ($found_slash) { + $i = $j; + $r = $this->info['line-height']->validate( + $line_height, + $config, + $context + ); + if ($r !== false) { + $final .= '/' . $r; + } + } + $final .= ' '; + $stage = 2; + break; + } + return false; + case 2: // attempting to catch font-family + $font_family = + implode(' ', array_slice($bits, $i, $size - $i)); + $r = $this->info['font-family']->validate( + $font_family, + $config, + $context + ); + if ($r !== false) { + $final .= $r . ' '; + // processing completed successfully + return rtrim($final); + } + return false; + } + } + return false; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/FontFamily.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/FontFamily.php new file mode 100644 index 000000000..74e24c881 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/FontFamily.php @@ -0,0 +1,219 @@ +<?php + +/** + * Validates a font family list according to CSS spec + */ +class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef +{ + + protected $mask = null; + + public function __construct() + { + $this->mask = '_- '; + for ($c = 'a'; $c <= 'z'; $c++) { + $this->mask .= $c; + } + for ($c = 'A'; $c <= 'Z'; $c++) { + $this->mask .= $c; + } + for ($c = '0'; $c <= '9'; $c++) { + $this->mask .= $c; + } // cast-y, but should be fine + // special bytes used by UTF-8 + for ($i = 0x80; $i <= 0xFF; $i++) { + // We don't bother excluding invalid bytes in this range, + // because the our restriction of well-formed UTF-8 will + // prevent these from ever occurring. + $this->mask .= chr($i); + } + + /* + PHP's internal strcspn implementation is + O(length of string * length of mask), making it inefficient + for large masks. However, it's still faster than + preg_match 8) + for (p = s1;;) { + spanp = s2; + do { + if (*spanp == c || p == s1_end) { + return p - s1; + } + } while (spanp++ < (s2_end - 1)); + c = *++p; + } + */ + // possible optimization: invert the mask. + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + static $generic_names = array( + 'serif' => true, + 'sans-serif' => true, + 'monospace' => true, + 'fantasy' => true, + 'cursive' => true + ); + $allowed_fonts = $config->get('CSS.AllowedFonts'); + + // assume that no font names contain commas in them + $fonts = explode(',', $string); + $final = ''; + foreach ($fonts as $font) { + $font = trim($font); + if ($font === '') { + continue; + } + // match a generic name + if (isset($generic_names[$font])) { + if ($allowed_fonts === null || isset($allowed_fonts[$font])) { + $final .= $font . ', '; + } + continue; + } + // match a quoted name + if ($font[0] === '"' || $font[0] === "'") { + $length = strlen($font); + if ($length <= 2) { + continue; + } + $quote = $font[0]; + if ($font[$length - 1] !== $quote) { + continue; + } + $font = substr($font, 1, $length - 2); + } + + $font = $this->expandCSSEscape($font); + + // $font is a pure representation of the font name + + if ($allowed_fonts !== null && !isset($allowed_fonts[$font])) { + continue; + } + + if (ctype_alnum($font) && $font !== '') { + // very simple font, allow it in unharmed + $final .= $font . ', '; + continue; + } + + // bugger out on whitespace. form feed (0C) really + // shouldn't show up regardless + $font = str_replace(array("\n", "\t", "\r", "\x0C"), ' ', $font); + + // Here, there are various classes of characters which need + // to be treated differently: + // - Alphanumeric characters are essentially safe. We + // handled these above. + // - Spaces require quoting, though most parsers will do + // the right thing if there aren't any characters that + // can be misinterpreted + // - Dashes rarely occur, but they fairly unproblematic + // for parsing/rendering purposes. + // The above characters cover the majority of Western font + // names. + // - Arbitrary Unicode characters not in ASCII. Because + // most parsers give little thought to Unicode, treatment + // of these codepoints is basically uniform, even for + // punctuation-like codepoints. These characters can + // show up in non-Western pages and are supported by most + // major browsers, for example: "MS 明朝" is a + // legitimate font-name + // <http://ja.wikipedia.org/wiki/MS_明朝>. See + // the CSS3 spec for more examples: + // <http://www.w3.org/TR/2011/WD-css3-fonts-20110324/localizedfamilynames.png> + // You can see live samples of these on the Internet: + // <http://www.google.co.jp/search?q=font-family+MS+明朝|ゴシック> + // However, most of these fonts have ASCII equivalents: + // for example, 'MS Mincho', and it's considered + // professional to use ASCII font names instead of + // Unicode font names. Thanks Takeshi Terada for + // providing this information. + // The following characters, to my knowledge, have not been + // used to name font names. + // - Single quote. While theoretically you might find a + // font name that has a single quote in its name (serving + // as an apostrophe, e.g. Dave's Scribble), I haven't + // been able to find any actual examples of this. + // Internet Explorer's cssText translation (which I + // believe is invoked by innerHTML) normalizes any + // quoting to single quotes, and fails to escape single + // quotes. (Note that this is not IE's behavior for all + // CSS properties, just some sort of special casing for + // font-family). So a single quote *cannot* be used + // safely in the font-family context if there will be an + // innerHTML/cssText translation. Note that Firefox 3.x + // does this too. + // - Double quote. In IE, these get normalized to + // single-quotes, no matter what the encoding. (Fun + // fact, in IE8, the 'content' CSS property gained + // support, where they special cased to preserve encoded + // double quotes, but still translate unadorned double + // quotes into single quotes.) So, because their + // fixpoint behavior is identical to single quotes, they + // cannot be allowed either. Firefox 3.x displays + // single-quote style behavior. + // - Backslashes are reduced by one (so \\ -> \) every + // iteration, so they cannot be used safely. This shows + // up in IE7, IE8 and FF3 + // - Semicolons, commas and backticks are handled properly. + // - The rest of the ASCII punctuation is handled properly. + // We haven't checked what browsers do to unadorned + // versions, but this is not important as long as the + // browser doesn't /remove/ surrounding quotes (as IE does + // for HTML). + // + // With these results in hand, we conclude that there are + // various levels of safety: + // - Paranoid: alphanumeric, spaces and dashes(?) + // - International: Paranoid + non-ASCII Unicode + // - Edgy: Everything except quotes, backslashes + // - NoJS: Standards compliance, e.g. sod IE. Note that + // with some judicious character escaping (since certain + // types of escaping doesn't work) this is theoretically + // OK as long as innerHTML/cssText is not called. + // We believe that international is a reasonable default + // (that we will implement now), and once we do more + // extensive research, we may feel comfortable with dropping + // it down to edgy. + + // Edgy: alphanumeric, spaces, dashes, underscores and Unicode. Use of + // str(c)spn assumes that the string was already well formed + // Unicode (which of course it is). + if (strspn($font, $this->mask) !== strlen($font)) { + continue; + } + + // Historical: + // In the absence of innerHTML/cssText, these ugly + // transforms don't pose a security risk (as \\ and \" + // might--these escapes are not supported by most browsers). + // We could try to be clever and use single-quote wrapping + // when there is a double quote present, but I have choosen + // not to implement that. (NOTE: you can reduce the amount + // of escapes by one depending on what quoting style you use) + // $font = str_replace('\\', '\\5C ', $font); + // $font = str_replace('"', '\\22 ', $font); + // $font = str_replace("'", '\\27 ', $font); + + // font possibly with spaces, requires quoting + $final .= "'$font', "; + } + $final = rtrim($final, ', '); + if ($final === '') { + return false; + } + return $final; + } + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ident.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ident.php new file mode 100644 index 000000000..973002c17 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ident.php @@ -0,0 +1,32 @@ +<?php + +/** + * Validates based on {ident} CSS grammar production + */ +class HTMLPurifier_AttrDef_CSS_Ident extends HTMLPurifier_AttrDef +{ + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = trim($string); + + // early abort: '' and '0' (strings that convert to false) are invalid + if (!$string) { + return false; + } + + $pattern = '/^(-?[A-Za-z_][A-Za-z_\-0-9]*)$/'; + if (!preg_match($pattern, $string)) { + return false; + } + return $string; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/ImportantDecorator.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/ImportantDecorator.php new file mode 100644 index 000000000..ffc989fe8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/ImportantDecorator.php @@ -0,0 +1,56 @@ +<?php + +/** + * Decorator which enables !important to be used in CSS values. + */ +class HTMLPurifier_AttrDef_CSS_ImportantDecorator extends HTMLPurifier_AttrDef +{ + /** + * @type HTMLPurifier_AttrDef + */ + public $def; + /** + * @type bool + */ + public $allow; + + /** + * @param HTMLPurifier_AttrDef $def Definition to wrap + * @param bool $allow Whether or not to allow !important + */ + public function __construct($def, $allow = false) + { + $this->def = $def; + $this->allow = $allow; + } + + /** + * Intercepts and removes !important if necessary + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + // test for ! and important tokens + $string = trim($string); + $is_important = false; + // :TODO: optimization: test directly for !important and ! important + if (strlen($string) >= 9 && substr($string, -9) === 'important') { + $temp = rtrim(substr($string, 0, -9)); + // use a temp, because we might want to restore important + if (strlen($temp) >= 1 && substr($temp, -1) === '!') { + $string = rtrim(substr($temp, 0, -1)); + $is_important = true; + } + } + $string = $this->def->validate($string, $config, $context); + if ($this->allow && $is_important) { + $string .= ' !important'; + } + return $string; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Length.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Length.php new file mode 100644 index 000000000..f12453a04 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Length.php @@ -0,0 +1,77 @@ +<?php + +/** + * Represents a Length as defined by CSS. + */ +class HTMLPurifier_AttrDef_CSS_Length extends HTMLPurifier_AttrDef +{ + + /** + * @type HTMLPurifier_Length|string + */ + protected $min; + + /** + * @type HTMLPurifier_Length|string + */ + protected $max; + + /** + * @param HTMLPurifier_Length|string $min Minimum length, or null for no bound. String is also acceptable. + * @param HTMLPurifier_Length|string $max Maximum length, or null for no bound. String is also acceptable. + */ + public function __construct($min = null, $max = null) + { + $this->min = $min !== null ? HTMLPurifier_Length::make($min) : null; + $this->max = $max !== null ? HTMLPurifier_Length::make($max) : null; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = $this->parseCDATA($string); + + // Optimizations + if ($string === '') { + return false; + } + if ($string === '0') { + return '0'; + } + if (strlen($string) === 1) { + return false; + } + + $length = HTMLPurifier_Length::make($string); + if (!$length->isValid()) { + return false; + } + + if ($this->min) { + $c = $length->compareTo($this->min); + if ($c === false) { + return false; + } + if ($c < 0) { + return false; + } + } + if ($this->max) { + $c = $length->compareTo($this->max); + if ($c === false) { + return false; + } + if ($c > 0) { + return false; + } + } + return $length->toString(); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/ListStyle.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/ListStyle.php new file mode 100644 index 000000000..e74d42654 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/ListStyle.php @@ -0,0 +1,112 @@ +<?php + +/** + * Validates shorthand CSS property list-style. + * @warning Does not support url tokens that have internal spaces. + */ +class HTMLPurifier_AttrDef_CSS_ListStyle extends HTMLPurifier_AttrDef +{ + + /** + * Local copy of validators. + * @type HTMLPurifier_AttrDef[] + * @note See HTMLPurifier_AttrDef_CSS_Font::$info for a similar impl. + */ + protected $info; + + /** + * @param HTMLPurifier_Config $config + */ + public function __construct($config) + { + $def = $config->getCSSDefinition(); + $this->info['list-style-type'] = $def->info['list-style-type']; + $this->info['list-style-position'] = $def->info['list-style-position']; + $this->info['list-style-image'] = $def->info['list-style-image']; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + // regular pre-processing + $string = $this->parseCDATA($string); + if ($string === '') { + return false; + } + + // assumes URI doesn't have spaces in it + $bits = explode(' ', strtolower($string)); // bits to process + + $caught = array(); + $caught['type'] = false; + $caught['position'] = false; + $caught['image'] = false; + + $i = 0; // number of catches + $none = false; + + foreach ($bits as $bit) { + if ($i >= 3) { + return; + } // optimization bit + if ($bit === '') { + continue; + } + foreach ($caught as $key => $status) { + if ($status !== false) { + continue; + } + $r = $this->info['list-style-' . $key]->validate($bit, $config, $context); + if ($r === false) { + continue; + } + if ($r === 'none') { + if ($none) { + continue; + } else { + $none = true; + } + if ($key == 'image') { + continue; + } + } + $caught[$key] = $r; + $i++; + break; + } + } + + if (!$i) { + return false; + } + + $ret = array(); + + // construct type + if ($caught['type']) { + $ret[] = $caught['type']; + } + + // construct image + if ($caught['image']) { + $ret[] = $caught['image']; + } + + // construct position + if ($caught['position']) { + $ret[] = $caught['position']; + } + + if (empty($ret)) { + return false; + } + return implode(' ', $ret); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Multiple.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Multiple.php new file mode 100644 index 000000000..e707f871c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Multiple.php @@ -0,0 +1,71 @@ +<?php + +/** + * Framework class for strings that involve multiple values. + * + * Certain CSS properties such as border-width and margin allow multiple + * lengths to be specified. This class can take a vanilla border-width + * definition and multiply it, usually into a max of four. + * + * @note Even though the CSS specification isn't clear about it, inherit + * can only be used alone: it will never manifest as part of a multi + * shorthand declaration. Thus, this class does not allow inherit. + */ +class HTMLPurifier_AttrDef_CSS_Multiple extends HTMLPurifier_AttrDef +{ + /** + * Instance of component definition to defer validation to. + * @type HTMLPurifier_AttrDef + * @todo Make protected + */ + public $single; + + /** + * Max number of values allowed. + * @todo Make protected + */ + public $max; + + /** + * @param HTMLPurifier_AttrDef $single HTMLPurifier_AttrDef to multiply + * @param int $max Max number of values allowed (usually four) + */ + public function __construct($single, $max = 4) + { + $this->single = $single; + $this->max = $max; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = $this->mungeRgb($this->parseCDATA($string)); + if ($string === '') { + return false; + } + $parts = explode(' ', $string); // parseCDATA replaced \r, \t and \n + $length = count($parts); + $final = ''; + for ($i = 0, $num = 0; $i < $length && $num < $this->max; $i++) { + if (ctype_space($parts[$i])) { + continue; + } + $result = $this->single->validate($parts[$i], $config, $context); + if ($result !== false) { + $final .= $result . ' '; + $num++; + } + } + if ($final === '') { + return false; + } + return rtrim($final); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Number.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Number.php new file mode 100644 index 000000000..8edc159e7 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Number.php @@ -0,0 +1,84 @@ +<?php + +/** + * Validates a number as defined by the CSS spec. + */ +class HTMLPurifier_AttrDef_CSS_Number extends HTMLPurifier_AttrDef +{ + + /** + * Indicates whether or not only positive values are allowed. + * @type bool + */ + protected $non_negative = false; + + /** + * @param bool $non_negative indicates whether negatives are forbidden + */ + public function __construct($non_negative = false) + { + $this->non_negative = $non_negative; + } + + /** + * @param string $number + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string|bool + * @warning Some contexts do not pass $config, $context. These + * variables should not be used without checking HTMLPurifier_Length + */ + public function validate($number, $config, $context) + { + $number = $this->parseCDATA($number); + + if ($number === '') { + return false; + } + if ($number === '0') { + return '0'; + } + + $sign = ''; + switch ($number[0]) { + case '-': + if ($this->non_negative) { + return false; + } + $sign = '-'; + case '+': + $number = substr($number, 1); + } + + if (ctype_digit($number)) { + $number = ltrim($number, '0'); + return $number ? $sign . $number : '0'; + } + + // Period is the only non-numeric character allowed + if (strpos($number, '.') === false) { + return false; + } + + list($left, $right) = explode('.', $number, 2); + + if ($left === '' && $right === '') { + return false; + } + if ($left !== '' && !ctype_digit($left)) { + return false; + } + + $left = ltrim($left, '0'); + $right = rtrim($right, '0'); + + if ($right === '') { + return $left ? $sign . $left : '0'; + } elseif (!ctype_digit($right)) { + return false; + } + return $sign . $left . '.' . $right; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Percentage.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Percentage.php new file mode 100644 index 000000000..f0f25c50a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Percentage.php @@ -0,0 +1,54 @@ +<?php + +/** + * Validates a Percentage as defined by the CSS spec. + */ +class HTMLPurifier_AttrDef_CSS_Percentage extends HTMLPurifier_AttrDef +{ + + /** + * Instance to defer number validation to. + * @type HTMLPurifier_AttrDef_CSS_Number + */ + protected $number_def; + + /** + * @param bool $non_negative Whether to forbid negative values + */ + public function __construct($non_negative = false) + { + $this->number_def = new HTMLPurifier_AttrDef_CSS_Number($non_negative); + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = $this->parseCDATA($string); + + if ($string === '') { + return false; + } + $length = strlen($string); + if ($length === 1) { + return false; + } + if ($string[$length - 1] !== '%') { + return false; + } + + $number = substr($string, 0, $length - 1); + $number = $this->number_def->validate($number, $config, $context); + + if ($number === false) { + return false; + } + return "$number%"; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/TextDecoration.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/TextDecoration.php new file mode 100644 index 000000000..5fd4b7f7b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/TextDecoration.php @@ -0,0 +1,46 @@ +<?php + +/** + * Validates the value for the CSS property text-decoration + * @note This class could be generalized into a version that acts sort of + * like Enum except you can compound the allowed values. + */ +class HTMLPurifier_AttrDef_CSS_TextDecoration extends HTMLPurifier_AttrDef +{ + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + static $allowed_values = array( + 'line-through' => true, + 'overline' => true, + 'underline' => true, + ); + + $string = strtolower($this->parseCDATA($string)); + + if ($string === 'none') { + return $string; + } + + $parts = explode(' ', $string); + $final = ''; + foreach ($parts as $part) { + if (isset($allowed_values[$part])) { + $final .= $part . ' '; + } + } + $final = rtrim($final); + if ($final === '') { + return false; + } + return $final; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/URI.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/URI.php new file mode 100644 index 000000000..6617acace --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/URI.php @@ -0,0 +1,77 @@ +<?php + +/** + * Validates a URI in CSS syntax, which uses url('http://example.com') + * @note While theoretically speaking a URI in a CSS document could + * be non-embedded, as of CSS2 there is no such usage so we're + * generalizing it. This may need to be changed in the future. + * @warning Since HTMLPurifier_AttrDef_CSS blindly uses semicolons as + * the separator, you cannot put a literal semicolon in + * in the URI. Try percent encoding it, in that case. + */ +class HTMLPurifier_AttrDef_CSS_URI extends HTMLPurifier_AttrDef_URI +{ + + public function __construct() + { + parent::__construct(true); // always embedded + } + + /** + * @param string $uri_string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($uri_string, $config, $context) + { + // parse the URI out of the string and then pass it onto + // the parent object + + $uri_string = $this->parseCDATA($uri_string); + if (strpos($uri_string, 'url(') !== 0) { + return false; + } + $uri_string = substr($uri_string, 4); + if (strlen($uri_string) == 0) { + return false; + } + $new_length = strlen($uri_string) - 1; + if ($uri_string[$new_length] != ')') { + return false; + } + $uri = trim(substr($uri_string, 0, $new_length)); + + if (!empty($uri) && ($uri[0] == "'" || $uri[0] == '"')) { + $quote = $uri[0]; + $new_length = strlen($uri) - 1; + if ($uri[$new_length] !== $quote) { + return false; + } + $uri = substr($uri, 1, $new_length - 1); + } + + $uri = $this->expandCSSEscape($uri); + + $result = parent::validate($uri, $config, $context); + + if ($result === false) { + return false; + } + + // extra sanity check; should have been done by URI + $result = str_replace(array('"', "\\", "\n", "\x0c", "\r"), "", $result); + + // suspicious characters are ()'; we're going to percent encode + // them for safety. + $result = str_replace(array('(', ')', "'"), array('%28', '%29', '%27'), $result); + + // there's an extra bug where ampersands lose their escaping on + // an innerHTML cycle, so a very unlucky query parameter could + // then change the meaning of the URL. Unfortunately, there's + // not much we can do about that... + return "url(\"$result\")"; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Clone.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Clone.php new file mode 100644 index 000000000..6698a00c0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Clone.php @@ -0,0 +1,44 @@ +<?php + +/** + * Dummy AttrDef that mimics another AttrDef, BUT it generates clones + * with make. + */ +class HTMLPurifier_AttrDef_Clone extends HTMLPurifier_AttrDef +{ + /** + * What we're cloning. + * @type HTMLPurifier_AttrDef + */ + protected $clone; + + /** + * @param HTMLPurifier_AttrDef $clone + */ + public function __construct($clone) + { + $this->clone = $clone; + } + + /** + * @param string $v + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($v, $config, $context) + { + return $this->clone->validate($v, $config, $context); + } + + /** + * @param string $string + * @return HTMLPurifier_AttrDef + */ + public function make($string) + { + return clone $this->clone; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Enum.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Enum.php new file mode 100644 index 000000000..8abda7f6e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Enum.php @@ -0,0 +1,73 @@ +<?php + +// Enum = Enumerated +/** + * Validates a keyword against a list of valid values. + * @warning The case-insensitive compare of this function uses PHP's + * built-in strtolower and ctype_lower functions, which may + * cause problems with international comparisons + */ +class HTMLPurifier_AttrDef_Enum extends HTMLPurifier_AttrDef +{ + + /** + * Lookup table of valid values. + * @type array + * @todo Make protected + */ + public $valid_values = array(); + + /** + * Bool indicating whether or not enumeration is case sensitive. + * @note In general this is always case insensitive. + */ + protected $case_sensitive = false; // values according to W3C spec + + /** + * @param array $valid_values List of valid values + * @param bool $case_sensitive Whether or not case sensitive + */ + public function __construct($valid_values = array(), $case_sensitive = false) + { + $this->valid_values = array_flip($valid_values); + $this->case_sensitive = $case_sensitive; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = trim($string); + if (!$this->case_sensitive) { + // we may want to do full case-insensitive libraries + $string = ctype_lower($string) ? $string : strtolower($string); + } + $result = isset($this->valid_values[$string]); + + return $result ? $string : false; + } + + /** + * @param string $string In form of comma-delimited list of case-insensitive + * valid values. Example: "foo,bar,baz". Prepend "s:" to make + * case sensitive + * @return HTMLPurifier_AttrDef_Enum + */ + public function make($string) + { + if (strlen($string) > 2 && $string[0] == 's' && $string[1] == ':') { + $string = substr($string, 2); + $sensitive = true; + } else { + $sensitive = false; + } + $values = explode(',', $string); + return new HTMLPurifier_AttrDef_Enum($values, $sensitive); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Bool.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Bool.php new file mode 100644 index 000000000..dea15d2cd --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Bool.php @@ -0,0 +1,48 @@ +<?php + +/** + * Validates a boolean attribute + */ +class HTMLPurifier_AttrDef_HTML_Bool extends HTMLPurifier_AttrDef +{ + + /** + * @type bool + */ + protected $name; + + /** + * @type bool + */ + public $minimized = true; + + /** + * @param bool $name + */ + public function __construct($name = false) + { + $this->name = $name; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + return $this->name; + } + + /** + * @param string $string Name of attribute + * @return HTMLPurifier_AttrDef_HTML_Bool + */ + public function make($string) + { + return new HTMLPurifier_AttrDef_HTML_Bool($string); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Class.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Class.php new file mode 100644 index 000000000..d5013488f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Class.php @@ -0,0 +1,48 @@ +<?php + +/** + * Implements special behavior for class attribute (normally NMTOKENS) + */ +class HTMLPurifier_AttrDef_HTML_Class extends HTMLPurifier_AttrDef_HTML_Nmtokens +{ + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + protected function split($string, $config, $context) + { + // really, this twiddle should be lazy loaded + $name = $config->getDefinition('HTML')->doctype->name; + if ($name == "XHTML 1.1" || $name == "XHTML 2.0") { + return parent::split($string, $config, $context); + } else { + return preg_split('/\s+/', $string); + } + } + + /** + * @param array $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + protected function filter($tokens, $config, $context) + { + $allowed = $config->get('Attr.AllowedClasses'); + $forbidden = $config->get('Attr.ForbiddenClasses'); + $ret = array(); + foreach ($tokens as $token) { + if (($allowed === null || isset($allowed[$token])) && + !isset($forbidden[$token]) && + // We need this O(n) check because of PHP's array + // implementation that casts -0 to 0. + !in_array($token, $ret, true) + ) { + $ret[] = $token; + } + } + return $ret; + } +} diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Color.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Color.php new file mode 100644 index 000000000..946ebb782 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Color.php @@ -0,0 +1,51 @@ +<?php + +/** + * Validates a color according to the HTML spec. + */ +class HTMLPurifier_AttrDef_HTML_Color extends HTMLPurifier_AttrDef +{ + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + static $colors = null; + if ($colors === null) { + $colors = $config->get('Core.ColorKeywords'); + } + + $string = trim($string); + + if (empty($string)) { + return false; + } + $lower = strtolower($string); + if (isset($colors[$lower])) { + return $colors[$lower]; + } + if ($string[0] === '#') { + $hex = substr($string, 1); + } else { + $hex = $string; + } + + $length = strlen($hex); + if ($length !== 3 && $length !== 6) { + return false; + } + if (!ctype_xdigit($hex)) { + return false; + } + if ($length === 3) { + $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; + } + return "#$hex"; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/FrameTarget.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/FrameTarget.php new file mode 100644 index 000000000..d79ba12b3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/FrameTarget.php @@ -0,0 +1,38 @@ +<?php + +/** + * Special-case enum attribute definition that lazy loads allowed frame targets + */ +class HTMLPurifier_AttrDef_HTML_FrameTarget extends HTMLPurifier_AttrDef_Enum +{ + + /** + * @type array + */ + public $valid_values = false; // uninitialized value + + /** + * @type bool + */ + protected $case_sensitive = false; + + public function __construct() + { + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + if ($this->valid_values === false) { + $this->valid_values = $config->get('Attr.AllowedFrameTargets'); + } + return parent::validate($string, $config, $context); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/ID.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/ID.php new file mode 100644 index 000000000..4ba45610f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/ID.php @@ -0,0 +1,113 @@ +<?php + +/** + * Validates the HTML attribute ID. + * @warning Even though this is the id processor, it + * will ignore the directive Attr:IDBlacklist, since it will only + * go according to the ID accumulator. Since the accumulator is + * automatically generated, it will have already absorbed the + * blacklist. If you're hacking around, make sure you use load()! + */ + +class HTMLPurifier_AttrDef_HTML_ID extends HTMLPurifier_AttrDef +{ + + // selector is NOT a valid thing to use for IDREFs, because IDREFs + // *must* target IDs that exist, whereas selector #ids do not. + + /** + * Determines whether or not we're validating an ID in a CSS + * selector context. + * @type bool + */ + protected $selector; + + /** + * @param bool $selector + */ + public function __construct($selector = false) + { + $this->selector = $selector; + } + + /** + * @param string $id + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($id, $config, $context) + { + if (!$this->selector && !$config->get('Attr.EnableID')) { + return false; + } + + $id = trim($id); // trim it first + + if ($id === '') { + return false; + } + + $prefix = $config->get('Attr.IDPrefix'); + if ($prefix !== '') { + $prefix .= $config->get('Attr.IDPrefixLocal'); + // prevent re-appending the prefix + if (strpos($id, $prefix) !== 0) { + $id = $prefix . $id; + } + } elseif ($config->get('Attr.IDPrefixLocal') !== '') { + trigger_error( + '%Attr.IDPrefixLocal cannot be used unless ' . + '%Attr.IDPrefix is set', + E_USER_WARNING + ); + } + + if (!$this->selector) { + $id_accumulator =& $context->get('IDAccumulator'); + if (isset($id_accumulator->ids[$id])) { + return false; + } + } + + // we purposely avoid using regex, hopefully this is faster + + if ($config->get('Attr.ID.HTML5') === true) { + if (preg_match('/[\t\n\x0b\x0c ]/', $id)) { + return false; + } + } else { + if (ctype_alpha($id)) { + // OK + } else { + if (!ctype_alpha(@$id[0])) { + return false; + } + // primitive style of regexps, I suppose + $trim = trim( + $id, + 'A..Za..z0..9:-._' + ); + if ($trim !== '') { + return false; + } + } + } + + $regexp = $config->get('Attr.IDBlacklistRegexp'); + if ($regexp && preg_match($regexp, $id)) { + return false; + } + + if (!$this->selector) { + $id_accumulator->add($id); + } + + // if no change was made to the ID, return the result + // else, return the new id if stripping whitespace made it + // valid, or return false. + return $id; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Length.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Length.php new file mode 100644 index 000000000..1c4006fbb --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Length.php @@ -0,0 +1,56 @@ +<?php + +/** + * Validates the HTML type length (not to be confused with CSS's length). + * + * This accepts integer pixels or percentages as lengths for certain + * HTML attributes. + */ + +class HTMLPurifier_AttrDef_HTML_Length extends HTMLPurifier_AttrDef_HTML_Pixels +{ + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = trim($string); + if ($string === '') { + return false; + } + + $parent_result = parent::validate($string, $config, $context); + if ($parent_result !== false) { + return $parent_result; + } + + $length = strlen($string); + $last_char = $string[$length - 1]; + + if ($last_char !== '%') { + return false; + } + + $points = substr($string, 0, $length - 1); + + if (!is_numeric($points)) { + return false; + } + + $points = (int)$points; + + if ($points < 0) { + return '0%'; + } + if ($points > 100) { + return '100%'; + } + return ((string)$points) . '%'; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/LinkTypes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/LinkTypes.php new file mode 100644 index 000000000..63fa04c15 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/LinkTypes.php @@ -0,0 +1,72 @@ +<?php + +/** + * Validates a rel/rev link attribute against a directive of allowed values + * @note We cannot use Enum because link types allow multiple + * values. + * @note Assumes link types are ASCII text + */ +class HTMLPurifier_AttrDef_HTML_LinkTypes extends HTMLPurifier_AttrDef +{ + + /** + * Name config attribute to pull. + * @type string + */ + protected $name; + + /** + * @param string $name + */ + public function __construct($name) + { + $configLookup = array( + 'rel' => 'AllowedRel', + 'rev' => 'AllowedRev' + ); + if (!isset($configLookup[$name])) { + trigger_error( + 'Unrecognized attribute name for link ' . + 'relationship.', + E_USER_ERROR + ); + return; + } + $this->name = $configLookup[$name]; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $allowed = $config->get('Attr.' . $this->name); + if (empty($allowed)) { + return false; + } + + $string = $this->parseCDATA($string); + $parts = explode(' ', $string); + + // lookup to prevent duplicates + $ret_lookup = array(); + foreach ($parts as $part) { + $part = strtolower(trim($part)); + if (!isset($allowed[$part])) { + continue; + } + $ret_lookup[$part] = true; + } + + if (empty($ret_lookup)) { + return false; + } + $string = implode(' ', array_keys($ret_lookup)); + return $string; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/MultiLength.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/MultiLength.php new file mode 100644 index 000000000..bbb20f2f8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/MultiLength.php @@ -0,0 +1,60 @@ +<?php + +/** + * Validates a MultiLength as defined by the HTML spec. + * + * A multilength is either a integer (pixel count), a percentage, or + * a relative number. + */ +class HTMLPurifier_AttrDef_HTML_MultiLength extends HTMLPurifier_AttrDef_HTML_Length +{ + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = trim($string); + if ($string === '') { + return false; + } + + $parent_result = parent::validate($string, $config, $context); + if ($parent_result !== false) { + return $parent_result; + } + + $length = strlen($string); + $last_char = $string[$length - 1]; + + if ($last_char !== '*') { + return false; + } + + $int = substr($string, 0, $length - 1); + + if ($int == '') { + return '*'; + } + if (!is_numeric($int)) { + return false; + } + + $int = (int)$int; + if ($int < 0) { + return false; + } + if ($int == 0) { + return '0'; + } + if ($int == 1) { + return '*'; + } + return ((string)$int) . '*'; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Nmtokens.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Nmtokens.php new file mode 100644 index 000000000..f79683b4f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Nmtokens.php @@ -0,0 +1,70 @@ +<?php + +/** + * Validates contents based on NMTOKENS attribute type. + */ +class HTMLPurifier_AttrDef_HTML_Nmtokens extends HTMLPurifier_AttrDef +{ + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = trim($string); + + // early abort: '' and '0' (strings that convert to false) are invalid + if (!$string) { + return false; + } + + $tokens = $this->split($string, $config, $context); + $tokens = $this->filter($tokens, $config, $context); + if (empty($tokens)) { + return false; + } + return implode(' ', $tokens); + } + + /** + * Splits a space separated list of tokens into its constituent parts. + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + protected function split($string, $config, $context) + { + // OPTIMIZABLE! + // do the preg_match, capture all subpatterns for reformulation + + // we don't support U+00A1 and up codepoints or + // escaping because I don't know how to do that with regexps + // and plus it would complicate optimization efforts (you never + // see that anyway). + $pattern = '/(?:(?<=\s)|\A)' . // look behind for space or string start + '((?:--|-?[A-Za-z_])[A-Za-z_\-0-9]*)' . + '(?:(?=\s)|\z)/'; // look ahead for space or string end + preg_match_all($pattern, $string, $matches); + return $matches[1]; + } + + /** + * Template method for removing certain tokens based on arbitrary criteria. + * @note If we wanted to be really functional, we'd do an array_filter + * with a callback. But... we're not. + * @param array $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + protected function filter($tokens, $config, $context) + { + return $tokens; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Pixels.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Pixels.php new file mode 100644 index 000000000..a1d019e09 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Pixels.php @@ -0,0 +1,76 @@ +<?php + +/** + * Validates an integer representation of pixels according to the HTML spec. + */ +class HTMLPurifier_AttrDef_HTML_Pixels extends HTMLPurifier_AttrDef +{ + + /** + * @type int + */ + protected $max; + + /** + * @param int $max + */ + public function __construct($max = null) + { + $this->max = $max; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = trim($string); + if ($string === '0') { + return $string; + } + if ($string === '') { + return false; + } + $length = strlen($string); + if (substr($string, $length - 2) == 'px') { + $string = substr($string, 0, $length - 2); + } + if (!is_numeric($string)) { + return false; + } + $int = (int)$string; + + if ($int < 0) { + return '0'; + } + + // upper-bound value, extremely high values can + // crash operating systems, see <http://ha.ckers.org/imagecrash.html> + // WARNING, above link WILL crash you if you're using Windows + + if ($this->max !== null && $int > $this->max) { + return (string)$this->max; + } + return (string)$int; + } + + /** + * @param string $string + * @return HTMLPurifier_AttrDef + */ + public function make($string) + { + if ($string === '') { + $max = null; + } else { + $max = (int)$string; + } + $class = get_class($this); + return new $class($max); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Integer.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Integer.php new file mode 100644 index 000000000..400e707d2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Integer.php @@ -0,0 +1,91 @@ +<?php + +/** + * Validates an integer. + * @note While this class was modeled off the CSS definition, no currently + * allowed CSS uses this type. The properties that do are: widows, + * orphans, z-index, counter-increment, counter-reset. Some of the + * HTML attributes, however, find use for a non-negative version of this. + */ +class HTMLPurifier_AttrDef_Integer extends HTMLPurifier_AttrDef +{ + + /** + * Whether or not negative values are allowed. + * @type bool + */ + protected $negative = true; + + /** + * Whether or not zero is allowed. + * @type bool + */ + protected $zero = true; + + /** + * Whether or not positive values are allowed. + * @type bool + */ + protected $positive = true; + + /** + * @param $negative Bool indicating whether or not negative values are allowed + * @param $zero Bool indicating whether or not zero is allowed + * @param $positive Bool indicating whether or not positive values are allowed + */ + public function __construct($negative = true, $zero = true, $positive = true) + { + $this->negative = $negative; + $this->zero = $zero; + $this->positive = $positive; + } + + /** + * @param string $integer + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($integer, $config, $context) + { + $integer = $this->parseCDATA($integer); + if ($integer === '') { + return false; + } + + // we could possibly simply typecast it to integer, but there are + // certain fringe cases that must not return an integer. + + // clip leading sign + if ($this->negative && $integer[0] === '-') { + $digits = substr($integer, 1); + if ($digits === '0') { + $integer = '0'; + } // rm minus sign for zero + } elseif ($this->positive && $integer[0] === '+') { + $digits = $integer = substr($integer, 1); // rm unnecessary plus + } else { + $digits = $integer; + } + + // test if it's numeric + if (!ctype_digit($digits)) { + return false; + } + + // perform scope tests + if (!$this->zero && $integer == 0) { + return false; + } + if (!$this->positive && $integer > 0) { + return false; + } + if (!$this->negative && $integer < 0) { + return false; + } + + return $integer; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Lang.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Lang.php new file mode 100644 index 000000000..2a55cea64 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Lang.php @@ -0,0 +1,86 @@ +<?php + +/** + * Validates the HTML attribute lang, effectively a language code. + * @note Built according to RFC 3066, which obsoleted RFC 1766 + */ +class HTMLPurifier_AttrDef_Lang extends HTMLPurifier_AttrDef +{ + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = trim($string); + if (!$string) { + return false; + } + + $subtags = explode('-', $string); + $num_subtags = count($subtags); + + if ($num_subtags == 0) { // sanity check + return false; + } + + // process primary subtag : $subtags[0] + $length = strlen($subtags[0]); + switch ($length) { + case 0: + return false; + case 1: + if (!($subtags[0] == 'x' || $subtags[0] == 'i')) { + return false; + } + break; + case 2: + case 3: + if (!ctype_alpha($subtags[0])) { + return false; + } elseif (!ctype_lower($subtags[0])) { + $subtags[0] = strtolower($subtags[0]); + } + break; + default: + return false; + } + + $new_string = $subtags[0]; + if ($num_subtags == 1) { + return $new_string; + } + + // process second subtag : $subtags[1] + $length = strlen($subtags[1]); + if ($length == 0 || ($length == 1 && $subtags[1] != 'x') || $length > 8 || !ctype_alnum($subtags[1])) { + return $new_string; + } + if (!ctype_lower($subtags[1])) { + $subtags[1] = strtolower($subtags[1]); + } + + $new_string .= '-' . $subtags[1]; + if ($num_subtags == 2) { + return $new_string; + } + + // process all other subtags, index 2 and up + for ($i = 2; $i < $num_subtags; $i++) { + $length = strlen($subtags[$i]); + if ($length == 0 || $length > 8 || !ctype_alnum($subtags[$i])) { + return $new_string; + } + if (!ctype_lower($subtags[$i])) { + $subtags[$i] = strtolower($subtags[$i]); + } + $new_string .= '-' . $subtags[$i]; + } + return $new_string; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Switch.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Switch.php new file mode 100644 index 000000000..c7eb3199a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Switch.php @@ -0,0 +1,53 @@ +<?php + +/** + * Decorator that, depending on a token, switches between two definitions. + */ +class HTMLPurifier_AttrDef_Switch +{ + + /** + * @type string + */ + protected $tag; + + /** + * @type HTMLPurifier_AttrDef + */ + protected $withTag; + + /** + * @type HTMLPurifier_AttrDef + */ + protected $withoutTag; + + /** + * @param string $tag Tag name to switch upon + * @param HTMLPurifier_AttrDef $with_tag Call if token matches tag + * @param HTMLPurifier_AttrDef $without_tag Call if token doesn't match, or there is no token + */ + public function __construct($tag, $with_tag, $without_tag) + { + $this->tag = $tag; + $this->withTag = $with_tag; + $this->withoutTag = $without_tag; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $token = $context->get('CurrentToken', true); + if (!$token || $token->name !== $this->tag) { + return $this->withoutTag->validate($string, $config, $context); + } else { + return $this->withTag->validate($string, $config, $context); + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Text.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Text.php new file mode 100644 index 000000000..4553a4ea9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Text.php @@ -0,0 +1,21 @@ +<?php + +/** + * Validates arbitrary text according to the HTML spec. + */ +class HTMLPurifier_AttrDef_Text extends HTMLPurifier_AttrDef +{ + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + return $this->parseCDATA($string); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI.php new file mode 100644 index 000000000..c1cd89772 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI.php @@ -0,0 +1,111 @@ +<?php + +/** + * Validates a URI as defined by RFC 3986. + * @note Scheme-specific mechanics deferred to HTMLPurifier_URIScheme + */ +class HTMLPurifier_AttrDef_URI extends HTMLPurifier_AttrDef +{ + + /** + * @type HTMLPurifier_URIParser + */ + protected $parser; + + /** + * @type bool + */ + protected $embedsResource; + + /** + * @param bool $embeds_resource Does the URI here result in an extra HTTP request? + */ + public function __construct($embeds_resource = false) + { + $this->parser = new HTMLPurifier_URIParser(); + $this->embedsResource = (bool)$embeds_resource; + } + + /** + * @param string $string + * @return HTMLPurifier_AttrDef_URI + */ + public function make($string) + { + $embeds = ($string === 'embedded'); + return new HTMLPurifier_AttrDef_URI($embeds); + } + + /** + * @param string $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($uri, $config, $context) + { + if ($config->get('URI.Disable')) { + return false; + } + + $uri = $this->parseCDATA($uri); + + // parse the URI + $uri = $this->parser->parse($uri); + if ($uri === false) { + return false; + } + + // add embedded flag to context for validators + $context->register('EmbeddedURI', $this->embedsResource); + + $ok = false; + do { + + // generic validation + $result = $uri->validate($config, $context); + if (!$result) { + break; + } + + // chained filtering + $uri_def = $config->getDefinition('URI'); + $result = $uri_def->filter($uri, $config, $context); + if (!$result) { + break; + } + + // scheme-specific validation + $scheme_obj = $uri->getSchemeObj($config, $context); + if (!$scheme_obj) { + break; + } + if ($this->embedsResource && !$scheme_obj->browsable) { + break; + } + $result = $scheme_obj->validate($uri, $config, $context); + if (!$result) { + break; + } + + // Post chained filtering + $result = $uri_def->postFilter($uri, $config, $context); + if (!$result) { + break; + } + + // survived gauntlet + $ok = true; + + } while (false); + + $context->destroy('EmbeddedURI'); + if (!$ok) { + return false; + } + // back to string + return $uri->toString(); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Email.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Email.php new file mode 100644 index 000000000..daf32b764 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Email.php @@ -0,0 +1,20 @@ +<?php + +abstract class HTMLPurifier_AttrDef_URI_Email extends HTMLPurifier_AttrDef +{ + + /** + * Unpacks a mailbox into its display-name and address + * @param string $string + * @return mixed + */ + public function unpack($string) + { + // needs to be implemented + } + +} + +// sub-implementations + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Email/SimpleCheck.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Email/SimpleCheck.php new file mode 100644 index 000000000..52c0d5968 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Email/SimpleCheck.php @@ -0,0 +1,29 @@ +<?php + +/** + * Primitive email validation class based on the regexp found at + * http://www.regular-expressions.info/email.html + */ +class HTMLPurifier_AttrDef_URI_Email_SimpleCheck extends HTMLPurifier_AttrDef_URI_Email +{ + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + // no support for named mailboxes i.e. "Bob <bob@example.com>" + // that needs more percent encoding to be done + if ($string == '') { + return false; + } + $string = trim($string); + $result = preg_match('/^[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i', $string); + return $result ? $string : false; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php new file mode 100644 index 000000000..3b4d18674 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php @@ -0,0 +1,138 @@ +<?php + +/** + * Validates a host according to the IPv4, IPv6 and DNS (future) specifications. + */ +class HTMLPurifier_AttrDef_URI_Host extends HTMLPurifier_AttrDef +{ + + /** + * IPv4 sub-validator. + * @type HTMLPurifier_AttrDef_URI_IPv4 + */ + protected $ipv4; + + /** + * IPv6 sub-validator. + * @type HTMLPurifier_AttrDef_URI_IPv6 + */ + protected $ipv6; + + public function __construct() + { + $this->ipv4 = new HTMLPurifier_AttrDef_URI_IPv4(); + $this->ipv6 = new HTMLPurifier_AttrDef_URI_IPv6(); + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $length = strlen($string); + // empty hostname is OK; it's usually semantically equivalent: + // the default host as defined by a URI scheme is used: + // + // If the URI scheme defines a default for host, then that + // default applies when the host subcomponent is undefined + // or when the registered name is empty (zero length). + if ($string === '') { + return ''; + } + if ($length > 1 && $string[0] === '[' && $string[$length - 1] === ']') { + //IPv6 + $ip = substr($string, 1, $length - 2); + $valid = $this->ipv6->validate($ip, $config, $context); + if ($valid === false) { + return false; + } + return '[' . $valid . ']'; + } + + // need to do checks on unusual encodings too + $ipv4 = $this->ipv4->validate($string, $config, $context); + if ($ipv4 !== false) { + return $ipv4; + } + + // A regular domain name. + + // This doesn't match I18N domain names, but we don't have proper IRI support, + // so force users to insert Punycode. + + // There is not a good sense in which underscores should be + // allowed, since it's technically not! (And if you go as + // far to allow everything as specified by the DNS spec... + // well, that's literally everything, modulo some space limits + // for the components and the overall name (which, by the way, + // we are NOT checking!). So we (arbitrarily) decide this: + // let's allow underscores wherever we would have allowed + // hyphens, if they are enabled. This is a pretty good match + // for browser behavior, for example, a large number of browsers + // cannot handle foo_.example.com, but foo_bar.example.com is + // fairly well supported. + $underscore = $config->get('Core.AllowHostnameUnderscore') ? '_' : ''; + + // Based off of RFC 1738, but amended so that + // as per RFC 3696, the top label need only not be all numeric. + // The productions describing this are: + $a = '[a-z]'; // alpha + $an = '[a-z0-9]'; // alphanum + $and = "[a-z0-9-$underscore]"; // alphanum | "-" + // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum + $domainlabel = "$an(?:$and*$an)?"; + // AMENDED as per RFC 3696 + // toplabel = alphanum | alphanum *( alphanum | "-" ) alphanum + // side condition: not all numeric + $toplabel = "$an(?:$and*$an)?"; + // hostname = *( domainlabel "." ) toplabel [ "." ] + if (preg_match("/^(?:$domainlabel\.)*($toplabel)\.?$/i", $string, $matches)) { + if (!ctype_digit($matches[1])) { + return $string; + } + } + + // PHP 5.3 and later support this functionality natively + if (function_exists('idn_to_ascii')) { + $string = idn_to_ascii($string); + + // If we have Net_IDNA2 support, we can support IRIs by + // punycoding them. (This is the most portable thing to do, + // since otherwise we have to assume browsers support + } elseif ($config->get('Core.EnableIDNA')) { + $idna = new Net_IDNA2(array('encoding' => 'utf8', 'overlong' => false, 'strict' => true)); + // we need to encode each period separately + $parts = explode('.', $string); + try { + $new_parts = array(); + foreach ($parts as $part) { + $encodable = false; + for ($i = 0, $c = strlen($part); $i < $c; $i++) { + if (ord($part[$i]) > 0x7a) { + $encodable = true; + break; + } + } + if (!$encodable) { + $new_parts[] = $part; + } else { + $new_parts[] = $idna->encode($part); + } + } + $string = implode('.', $new_parts); + } catch (Exception $e) { + // XXX error reporting + } + } + // Try again + if (preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string)) { + return $string; + } + return false; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv4.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv4.php new file mode 100644 index 000000000..30ac16c9e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv4.php @@ -0,0 +1,45 @@ +<?php + +/** + * Validates an IPv4 address + * @author Feyd @ forums.devnetwork.net (public domain) + */ +class HTMLPurifier_AttrDef_URI_IPv4 extends HTMLPurifier_AttrDef +{ + + /** + * IPv4 regex, protected so that IPv6 can reuse it. + * @type string + */ + protected $ip4; + + /** + * @param string $aIP + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($aIP, $config, $context) + { + if (!$this->ip4) { + $this->_loadRegex(); + } + + if (preg_match('#^' . $this->ip4 . '$#s', $aIP)) { + return $aIP; + } + return false; + } + + /** + * Lazy load function to prevent regex from being stuffed in + * cache. + */ + protected function _loadRegex() + { + $oct = '(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])'; // 0-255 + $this->ip4 = "(?:{$oct}\\.{$oct}\\.{$oct}\\.{$oct})"; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv6.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv6.php new file mode 100644 index 000000000..f243793ee --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv6.php @@ -0,0 +1,89 @@ +<?php + +/** + * Validates an IPv6 address. + * @author Feyd @ forums.devnetwork.net (public domain) + * @note This function requires brackets to have been removed from address + * in URI. + */ +class HTMLPurifier_AttrDef_URI_IPv6 extends HTMLPurifier_AttrDef_URI_IPv4 +{ + + /** + * @param string $aIP + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($aIP, $config, $context) + { + if (!$this->ip4) { + $this->_loadRegex(); + } + + $original = $aIP; + + $hex = '[0-9a-fA-F]'; + $blk = '(?:' . $hex . '{1,4})'; + $pre = '(?:/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))'; // /0 - /128 + + // prefix check + if (strpos($aIP, '/') !== false) { + if (preg_match('#' . $pre . '$#s', $aIP, $find)) { + $aIP = substr($aIP, 0, 0 - strlen($find[0])); + unset($find); + } else { + return false; + } + } + + // IPv4-compatiblity check + if (preg_match('#(?<=:' . ')' . $this->ip4 . '$#s', $aIP, $find)) { + $aIP = substr($aIP, 0, 0 - strlen($find[0])); + $ip = explode('.', $find[0]); + $ip = array_map('dechex', $ip); + $aIP .= $ip[0] . $ip[1] . ':' . $ip[2] . $ip[3]; + unset($find, $ip); + } + + // compression check + $aIP = explode('::', $aIP); + $c = count($aIP); + if ($c > 2) { + return false; + } elseif ($c == 2) { + list($first, $second) = $aIP; + $first = explode(':', $first); + $second = explode(':', $second); + + if (count($first) + count($second) > 8) { + return false; + } + + while (count($first) < 8) { + array_push($first, '0'); + } + + array_splice($first, 8 - count($second), 8, $second); + $aIP = $first; + unset($first, $second); + } else { + $aIP = explode(':', $aIP[0]); + } + $c = count($aIP); + + if ($c != 8) { + return false; + } + + // All the pieces should be 16-bit hex strings. Are they? + foreach ($aIP as $piece) { + if (!preg_match('#^[0-9a-fA-F]{4}$#s', sprintf('%04s', $piece))) { + return false; + } + } + return $original; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform.php new file mode 100644 index 000000000..b428331f1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform.php @@ -0,0 +1,60 @@ +<?php + +/** + * Processes an entire attribute array for corrections needing multiple values. + * + * Occasionally, a certain attribute will need to be removed and popped onto + * another value. Instead of creating a complex return syntax for + * HTMLPurifier_AttrDef, we just pass the whole attribute array to a + * specialized object and have that do the special work. That is the + * family of HTMLPurifier_AttrTransform. + * + * An attribute transformation can be assigned to run before or after + * HTMLPurifier_AttrDef validation. See HTMLPurifier_HTMLDefinition for + * more details. + */ + +abstract class HTMLPurifier_AttrTransform +{ + + /** + * Abstract: makes changes to the attributes dependent on multiple values. + * + * @param array $attr Assoc array of attributes, usually from + * HTMLPurifier_Token_Tag::$attr + * @param HTMLPurifier_Config $config Mandatory HTMLPurifier_Config object. + * @param HTMLPurifier_Context $context Mandatory HTMLPurifier_Context object + * @return array Processed attribute array. + */ + abstract public function transform($attr, $config, $context); + + /** + * Prepends CSS properties to the style attribute, creating the + * attribute if it doesn't exist. + * @param array &$attr Attribute array to process (passed by reference) + * @param string $css CSS to prepend + */ + public function prependCSS(&$attr, $css) + { + $attr['style'] = isset($attr['style']) ? $attr['style'] : ''; + $attr['style'] = $css . $attr['style']; + } + + /** + * Retrieves and removes an attribute + * @param array &$attr Attribute array to process (passed by reference) + * @param mixed $key Key of attribute to confiscate + * @return mixed + */ + public function confiscateAttr(&$attr, $key) + { + if (!isset($attr[$key])) { + return null; + } + $value = $attr[$key]; + unset($attr[$key]); + return $value; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Background.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Background.php new file mode 100644 index 000000000..2f72869a5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Background.php @@ -0,0 +1,28 @@ +<?php + +/** + * Pre-transform that changes proprietary background attribute to CSS. + */ +class HTMLPurifier_AttrTransform_Background extends HTMLPurifier_AttrTransform +{ + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['background'])) { + return $attr; + } + + $background = $this->confiscateAttr($attr, 'background'); + // some validation should happen here + + $this->prependCSS($attr, "background-image:url($background);"); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BdoDir.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BdoDir.php new file mode 100644 index 000000000..d66c04a5b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BdoDir.php @@ -0,0 +1,27 @@ +<?php + +// this MUST be placed in post, as it assumes that any value in dir is valid + +/** + * Post-trasnform that ensures that bdo tags have the dir attribute set. + */ +class HTMLPurifier_AttrTransform_BdoDir extends HTMLPurifier_AttrTransform +{ + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (isset($attr['dir'])) { + return $attr; + } + $attr['dir'] = $config->get('Attr.DefaultTextDir'); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BgColor.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BgColor.php new file mode 100644 index 000000000..0f51fd2ce --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BgColor.php @@ -0,0 +1,28 @@ +<?php + +/** + * Pre-transform that changes deprecated bgcolor attribute to CSS. + */ +class HTMLPurifier_AttrTransform_BgColor extends HTMLPurifier_AttrTransform +{ + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['bgcolor'])) { + return $attr; + } + + $bgcolor = $this->confiscateAttr($attr, 'bgcolor'); + // some validation should happen here + + $this->prependCSS($attr, "background-color:$bgcolor;"); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BoolToCSS.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BoolToCSS.php new file mode 100644 index 000000000..f25cd0195 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BoolToCSS.php @@ -0,0 +1,47 @@ +<?php + +/** + * Pre-transform that changes converts a boolean attribute to fixed CSS + */ +class HTMLPurifier_AttrTransform_BoolToCSS extends HTMLPurifier_AttrTransform +{ + /** + * Name of boolean attribute that is trigger. + * @type string + */ + protected $attr; + + /** + * CSS declarations to add to style, needs trailing semicolon. + * @type string + */ + protected $css; + + /** + * @param string $attr attribute name to convert from + * @param string $css CSS declarations to add to style (needs semicolon) + */ + public function __construct($attr, $css) + { + $this->attr = $attr; + $this->css = $css; + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr[$this->attr])) { + return $attr; + } + unset($attr[$this->attr]); + $this->prependCSS($attr, $this->css); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Border.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Border.php new file mode 100644 index 000000000..057dc017f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Border.php @@ -0,0 +1,26 @@ +<?php + +/** + * Pre-transform that changes deprecated border attribute to CSS. + */ +class HTMLPurifier_AttrTransform_Border extends HTMLPurifier_AttrTransform +{ + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['border'])) { + return $attr; + } + $border_width = $this->confiscateAttr($attr, 'border'); + // some validation should happen here + $this->prependCSS($attr, "border:{$border_width}px solid;"); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/EnumToCSS.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/EnumToCSS.php new file mode 100644 index 000000000..7ccd0e3fb --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/EnumToCSS.php @@ -0,0 +1,68 @@ +<?php + +/** + * Generic pre-transform that converts an attribute with a fixed number of + * values (enumerated) to CSS. + */ +class HTMLPurifier_AttrTransform_EnumToCSS extends HTMLPurifier_AttrTransform +{ + /** + * Name of attribute to transform from. + * @type string + */ + protected $attr; + + /** + * Lookup array of attribute values to CSS. + * @type array + */ + protected $enumToCSS = array(); + + /** + * Case sensitivity of the matching. + * @type bool + * @warning Currently can only be guaranteed to work with ASCII + * values. + */ + protected $caseSensitive = false; + + /** + * @param string $attr Attribute name to transform from + * @param array $enum_to_css Lookup array of attribute values to CSS + * @param bool $case_sensitive Case sensitivity indicator, default false + */ + public function __construct($attr, $enum_to_css, $case_sensitive = false) + { + $this->attr = $attr; + $this->enumToCSS = $enum_to_css; + $this->caseSensitive = (bool)$case_sensitive; + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr[$this->attr])) { + return $attr; + } + + $value = trim($attr[$this->attr]); + unset($attr[$this->attr]); + + if (!$this->caseSensitive) { + $value = strtolower($value); + } + + if (!isset($this->enumToCSS[$value])) { + return $attr; + } + $this->prependCSS($attr, $this->enumToCSS[$value]); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgRequired.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgRequired.php new file mode 100644 index 000000000..235ebb34b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgRequired.php @@ -0,0 +1,47 @@ +<?php + +// must be called POST validation + +/** + * Transform that supplies default values for the src and alt attributes + * in img tags, as well as prevents the img tag from being removed + * because of a missing alt tag. This needs to be registered as both + * a pre and post attribute transform. + */ +class HTMLPurifier_AttrTransform_ImgRequired extends HTMLPurifier_AttrTransform +{ + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + $src = true; + if (!isset($attr['src'])) { + if ($config->get('Core.RemoveInvalidImg')) { + return $attr; + } + $attr['src'] = $config->get('Attr.DefaultInvalidImage'); + $src = false; + } + + if (!isset($attr['alt'])) { + if ($src) { + $alt = $config->get('Attr.DefaultImageAlt'); + if ($alt === null) { + $attr['alt'] = basename($attr['src']); + } else { + $attr['alt'] = $alt; + } + } else { + $attr['alt'] = $config->get('Attr.DefaultInvalidImageAlt'); + } + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgSpace.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgSpace.php new file mode 100644 index 000000000..350b3358f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgSpace.php @@ -0,0 +1,61 @@ +<?php + +/** + * Pre-transform that changes deprecated hspace and vspace attributes to CSS + */ +class HTMLPurifier_AttrTransform_ImgSpace extends HTMLPurifier_AttrTransform +{ + /** + * @type string + */ + protected $attr; + + /** + * @type array + */ + protected $css = array( + 'hspace' => array('left', 'right'), + 'vspace' => array('top', 'bottom') + ); + + /** + * @param string $attr + */ + public function __construct($attr) + { + $this->attr = $attr; + if (!isset($this->css[$attr])) { + trigger_error(htmlspecialchars($attr) . ' is not valid space attribute'); + } + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr[$this->attr])) { + return $attr; + } + + $width = $this->confiscateAttr($attr, $this->attr); + // some validation could happen here + + if (!isset($this->css[$this->attr])) { + return $attr; + } + + $style = ''; + foreach ($this->css[$this->attr] as $suffix) { + $property = "margin-$suffix"; + $style .= "$property:{$width}px;"; + } + $this->prependCSS($attr, $style); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Input.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Input.php new file mode 100644 index 000000000..3ab47ed8c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Input.php @@ -0,0 +1,56 @@ +<?php + +/** + * Performs miscellaneous cross attribute validation and filtering for + * input elements. This is meant to be a post-transform. + */ +class HTMLPurifier_AttrTransform_Input extends HTMLPurifier_AttrTransform +{ + /** + * @type HTMLPurifier_AttrDef_HTML_Pixels + */ + protected $pixels; + + public function __construct() + { + $this->pixels = new HTMLPurifier_AttrDef_HTML_Pixels(); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['type'])) { + $t = 'text'; + } else { + $t = strtolower($attr['type']); + } + if (isset($attr['checked']) && $t !== 'radio' && $t !== 'checkbox') { + unset($attr['checked']); + } + if (isset($attr['maxlength']) && $t !== 'text' && $t !== 'password') { + unset($attr['maxlength']); + } + if (isset($attr['size']) && $t !== 'text' && $t !== 'password') { + $result = $this->pixels->validate($attr['size'], $config, $context); + if ($result === false) { + unset($attr['size']); + } else { + $attr['size'] = $result; + } + } + if (isset($attr['src']) && $t !== 'image') { + unset($attr['src']); + } + if (!isset($attr['value']) && ($t === 'radio' || $t === 'checkbox')) { + $attr['value'] = ''; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Lang.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Lang.php new file mode 100644 index 000000000..5b0aff0e4 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Lang.php @@ -0,0 +1,31 @@ +<?php + +/** + * Post-transform that copies lang's value to xml:lang (and vice-versa) + * @note Theoretically speaking, this could be a pre-transform, but putting + * post is more efficient. + */ +class HTMLPurifier_AttrTransform_Lang extends HTMLPurifier_AttrTransform +{ + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + $lang = isset($attr['lang']) ? $attr['lang'] : false; + $xml_lang = isset($attr['xml:lang']) ? $attr['xml:lang'] : false; + + if ($lang !== false && $xml_lang === false) { + $attr['xml:lang'] = $lang; + } elseif ($xml_lang !== false) { + $attr['lang'] = $xml_lang; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Length.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Length.php new file mode 100644 index 000000000..853f33549 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Length.php @@ -0,0 +1,45 @@ +<?php + +/** + * Class for handling width/height length attribute transformations to CSS + */ +class HTMLPurifier_AttrTransform_Length extends HTMLPurifier_AttrTransform +{ + + /** + * @type string + */ + protected $name; + + /** + * @type string + */ + protected $cssName; + + public function __construct($name, $css_name = null) + { + $this->name = $name; + $this->cssName = $css_name ? $css_name : $name; + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr[$this->name])) { + return $attr; + } + $length = $this->confiscateAttr($attr, $this->name); + if (ctype_digit($length)) { + $length .= 'px'; + } + $this->prependCSS($attr, $this->cssName . ":$length;"); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Name.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Name.php new file mode 100644 index 000000000..63cce6837 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Name.php @@ -0,0 +1,33 @@ +<?php + +/** + * Pre-transform that changes deprecated name attribute to ID if necessary + */ +class HTMLPurifier_AttrTransform_Name extends HTMLPurifier_AttrTransform +{ + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + // Abort early if we're using relaxed definition of name + if ($config->get('HTML.Attr.Name.UseCDATA')) { + return $attr; + } + if (!isset($attr['name'])) { + return $attr; + } + $id = $this->confiscateAttr($attr, 'name'); + if (isset($attr['id'])) { + return $attr; + } + $attr['id'] = $id; + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/NameSync.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/NameSync.php new file mode 100644 index 000000000..36079b786 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/NameSync.php @@ -0,0 +1,41 @@ +<?php + +/** + * Post-transform that performs validation to the name attribute; if + * it is present with an equivalent id attribute, it is passed through; + * otherwise validation is performed. + */ +class HTMLPurifier_AttrTransform_NameSync extends HTMLPurifier_AttrTransform +{ + + public function __construct() + { + $this->idDef = new HTMLPurifier_AttrDef_HTML_ID(); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['name'])) { + return $attr; + } + $name = $attr['name']; + if (isset($attr['id']) && $attr['id'] === $name) { + return $attr; + } + $result = $this->idDef->validate($name, $config, $context); + if ($result === false) { + unset($attr['name']); + } else { + $attr['name'] = $result; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Nofollow.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Nofollow.php new file mode 100644 index 000000000..1057ebee1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Nofollow.php @@ -0,0 +1,52 @@ +<?php + +// must be called POST validation + +/** + * Adds rel="nofollow" to all outbound links. This transform is + * only attached if Attr.Nofollow is TRUE. + */ +class HTMLPurifier_AttrTransform_Nofollow extends HTMLPurifier_AttrTransform +{ + /** + * @type HTMLPurifier_URIParser + */ + private $parser; + + public function __construct() + { + $this->parser = new HTMLPurifier_URIParser(); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['href'])) { + return $attr; + } + + // XXX Kind of inefficient + $url = $this->parser->parse($attr['href']); + $scheme = $url->getSchemeObj($config, $context); + + if ($scheme->browsable && !$url->isLocal($config, $context)) { + if (isset($attr['rel'])) { + $rels = explode(' ', $attr['rel']); + if (!in_array('nofollow', $rels)) { + $rels[] = 'nofollow'; + } + $attr['rel'] = implode(' ', $rels); + } else { + $attr['rel'] = 'nofollow'; + } + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeEmbed.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeEmbed.php new file mode 100644 index 000000000..231c81a3f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeEmbed.php @@ -0,0 +1,25 @@ +<?php + +class HTMLPurifier_AttrTransform_SafeEmbed extends HTMLPurifier_AttrTransform +{ + /** + * @type string + */ + public $name = "SafeEmbed"; + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + $attr['allowscriptaccess'] = 'never'; + $attr['allownetworking'] = 'internal'; + $attr['type'] = 'application/x-shockwave-flash'; + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeObject.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeObject.php new file mode 100644 index 000000000..d1f3a4d2e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeObject.php @@ -0,0 +1,28 @@ +<?php + +/** + * Writes default type for all objects. Currently only supports flash. + */ +class HTMLPurifier_AttrTransform_SafeObject extends HTMLPurifier_AttrTransform +{ + /** + * @type string + */ + public $name = "SafeObject"; + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['type'])) { + $attr['type'] = 'application/x-shockwave-flash'; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeParam.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeParam.php new file mode 100644 index 000000000..1143b4b49 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeParam.php @@ -0,0 +1,79 @@ +<?php + +/** + * Validates name/value pairs in param tags to be used in safe objects. This + * will only allow name values it recognizes, and pre-fill certain attributes + * with required values. + * + * @note + * This class only supports Flash. In the future, Quicktime support + * may be added. + * + * @warning + * This class expects an injector to add the necessary parameters tags. + */ +class HTMLPurifier_AttrTransform_SafeParam extends HTMLPurifier_AttrTransform +{ + /** + * @type string + */ + public $name = "SafeParam"; + + /** + * @type HTMLPurifier_AttrDef_URI + */ + private $uri; + + public function __construct() + { + $this->uri = new HTMLPurifier_AttrDef_URI(true); // embedded + $this->wmode = new HTMLPurifier_AttrDef_Enum(array('window', 'opaque', 'transparent')); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + // If we add support for other objects, we'll need to alter the + // transforms. + switch ($attr['name']) { + // application/x-shockwave-flash + // Keep this synchronized with Injector/SafeObject.php + case 'allowScriptAccess': + $attr['value'] = 'never'; + break; + case 'allowNetworking': + $attr['value'] = 'internal'; + break; + case 'allowFullScreen': + if ($config->get('HTML.FlashAllowFullScreen')) { + $attr['value'] = ($attr['value'] == 'true') ? 'true' : 'false'; + } else { + $attr['value'] = 'false'; + } + break; + case 'wmode': + $attr['value'] = $this->wmode->validate($attr['value'], $config, $context); + break; + case 'movie': + case 'src': + $attr['name'] = "movie"; + $attr['value'] = $this->uri->validate($attr['value'], $config, $context); + break; + case 'flashvars': + // we're going to allow arbitrary inputs to the SWF, on + // the reasoning that it could only hack the SWF, not us. + break; + // add other cases to support other param name/value pairs + default: + $attr['name'] = $attr['value'] = null; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ScriptRequired.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ScriptRequired.php new file mode 100644 index 000000000..b7057bbf8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ScriptRequired.php @@ -0,0 +1,23 @@ +<?php + +/** + * Implements required attribute stipulation for <script> + */ +class HTMLPurifier_AttrTransform_ScriptRequired extends HTMLPurifier_AttrTransform +{ + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['type'])) { + $attr['type'] = 'text/javascript'; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetBlank.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetBlank.php new file mode 100644 index 000000000..dd63ea89c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetBlank.php @@ -0,0 +1,45 @@ +<?php + +// must be called POST validation + +/** + * Adds target="blank" to all outbound links. This transform is + * only attached if Attr.TargetBlank is TRUE. This works regardless + * of whether or not Attr.AllowedFrameTargets + */ +class HTMLPurifier_AttrTransform_TargetBlank extends HTMLPurifier_AttrTransform +{ + /** + * @type HTMLPurifier_URIParser + */ + private $parser; + + public function __construct() + { + $this->parser = new HTMLPurifier_URIParser(); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['href'])) { + return $attr; + } + + // XXX Kind of inefficient + $url = $this->parser->parse($attr['href']); + $scheme = $url->getSchemeObj($config, $context); + + if ($scheme->browsable && !$url->isBenign($config, $context)) { + $attr['target'] = '_blank'; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetNoopener.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetNoopener.php new file mode 100644 index 000000000..1db3c6c09 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetNoopener.php @@ -0,0 +1,37 @@ +<?php + +// must be called POST validation + +/** + * Adds rel="noopener" to any links which target a different window + * than the current one. This is used to prevent malicious websites + * from silently replacing the original window, which could be used + * to do phishing. + * This transform is controlled by %HTML.TargetNoopener. + */ +class HTMLPurifier_AttrTransform_TargetNoopener extends HTMLPurifier_AttrTransform +{ + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (isset($attr['rel'])) { + $rels = explode(' ', $attr['rel']); + } else { + $rels = array(); + } + if (isset($attr['target']) && !in_array('noopener', $rels)) { + $rels[] = 'noopener'; + } + if (!empty($rels) || isset($attr['rel'])) { + $attr['rel'] = implode(' ', $rels); + } + + return $attr; + } +} + diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetNoreferrer.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetNoreferrer.php new file mode 100644 index 000000000..587dc2e07 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetNoreferrer.php @@ -0,0 +1,37 @@ +<?php + +// must be called POST validation + +/** + * Adds rel="noreferrer" to any links which target a different window + * than the current one. This is used to prevent malicious websites + * from silently replacing the original window, which could be used + * to do phishing. + * This transform is controlled by %HTML.TargetNoreferrer. + */ +class HTMLPurifier_AttrTransform_TargetNoreferrer extends HTMLPurifier_AttrTransform +{ + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (isset($attr['rel'])) { + $rels = explode(' ', $attr['rel']); + } else { + $rels = array(); + } + if (isset($attr['target']) && !in_array('noreferrer', $rels)) { + $rels[] = 'noreferrer'; + } + if (!empty($rels) || isset($attr['rel'])) { + $attr['rel'] = implode(' ', $rels); + } + + return $attr; + } +} + diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Textarea.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Textarea.php new file mode 100644 index 000000000..6a9f33a0c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Textarea.php @@ -0,0 +1,27 @@ +<?php + +/** + * Sets height/width defaults for <textarea> + */ +class HTMLPurifier_AttrTransform_Textarea extends HTMLPurifier_AttrTransform +{ + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + // Calculated from Firefox + if (!isset($attr['cols'])) { + $attr['cols'] = '22'; + } + if (!isset($attr['rows'])) { + $attr['rows'] = '3'; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTypes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTypes.php new file mode 100644 index 000000000..3b70520b6 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTypes.php @@ -0,0 +1,96 @@ +<?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(); + + // 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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrValidator.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrValidator.php new file mode 100644 index 000000000..f97dc93ed --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrValidator.php @@ -0,0 +1,178 @@ +<?php + +/** + * Validates the attributes of a token. Doesn't manage required attributes + * very well. The only reason we factored this out was because RemoveForeignElements + * also needed it besides ValidateAttributes. + */ +class HTMLPurifier_AttrValidator +{ + + /** + * Validates the attributes of a token, mutating it as necessary. + * that has valid tokens + * @param HTMLPurifier_Token $token Token to validate. + * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config + * @param HTMLPurifier_Context $context Instance of HTMLPurifier_Context + */ + public function validateToken($token, $config, $context) + { + $definition = $config->getHTMLDefinition(); + $e =& $context->get('ErrorCollector', true); + + // initialize IDAccumulator if necessary + $ok =& $context->get('IDAccumulator', true); + if (!$ok) { + $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context); + $context->register('IDAccumulator', $id_accumulator); + } + + // initialize CurrentToken if necessary + $current_token =& $context->get('CurrentToken', true); + if (!$current_token) { + $context->register('CurrentToken', $token); + } + + if (!$token instanceof HTMLPurifier_Token_Start && + !$token instanceof HTMLPurifier_Token_Empty + ) { + return; + } + + // create alias to global definition array, see also $defs + // DEFINITION CALL + $d_defs = $definition->info_global_attr; + + // don't update token until the very end, to ensure an atomic update + $attr = $token->attr; + + // do global transformations (pre) + // nothing currently utilizes this + foreach ($definition->info_attr_transform_pre as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + // do local transformations only applicable to this element (pre) + // ex. <p align="right"> to <p style="text-align:right;"> + foreach ($definition->info[$token->name]->attr_transform_pre as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + // create alias to this element's attribute definition array, see + // also $d_defs (global attribute definition array) + // DEFINITION CALL + $defs = $definition->info[$token->name]->attr; + + $attr_key = false; + $context->register('CurrentAttr', $attr_key); + + // iterate through all the attribute keypairs + // Watch out for name collisions: $key has previously been used + foreach ($attr as $attr_key => $value) { + + // call the definition + if (isset($defs[$attr_key])) { + // there is a local definition defined + if ($defs[$attr_key] === false) { + // We've explicitly been told not to allow this element. + // This is usually when there's a global definition + // that must be overridden. + // Theoretically speaking, we could have a + // AttrDef_DenyAll, but this is faster! + $result = false; + } else { + // validate according to the element's definition + $result = $defs[$attr_key]->validate( + $value, + $config, + $context + ); + } + } elseif (isset($d_defs[$attr_key])) { + // there is a global definition defined, validate according + // to the global definition + $result = $d_defs[$attr_key]->validate( + $value, + $config, + $context + ); + } else { + // system never heard of the attribute? DELETE! + $result = false; + } + + // put the results into effect + if ($result === false || $result === null) { + // this is a generic error message that should replaced + // with more specific ones when possible + if ($e) { + $e->send(E_ERROR, 'AttrValidator: Attribute removed'); + } + + // remove the attribute + unset($attr[$attr_key]); + } elseif (is_string($result)) { + // generally, if a substitution is happening, there + // was some sort of implicit correction going on. We'll + // delegate it to the attribute classes to say exactly what. + + // simple substitution + $attr[$attr_key] = $result; + } else { + // nothing happens + } + + // we'd also want slightly more complicated substitution + // involving an array as the return value, + // although we're not sure how colliding attributes would + // resolve (certain ones would be completely overriden, + // others would prepend themselves). + } + + $context->destroy('CurrentAttr'); + + // post transforms + + // global (error reporting untested) + foreach ($definition->info_attr_transform_post as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + // local (error reporting untested) + foreach ($definition->info[$token->name]->attr_transform_post as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + $token->attr = $attr; + + // destroy CurrentToken if we made it ourselves + if (!$current_token) { + $context->destroy('CurrentToken'); + } + + } + + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Bootstrap.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Bootstrap.php new file mode 100644 index 000000000..707122bb2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Bootstrap.php @@ -0,0 +1,124 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/CSSDefinition.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/CSSDefinition.php new file mode 100644 index 000000000..47dfd1f66 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/CSSDefinition.php @@ -0,0 +1,491 @@ +<?php + +/** + * Defines allowed CSS attributes and what their values are. + * @see HTMLPurifier_HTMLDefinition + */ +class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition +{ + + public $type = 'CSS'; + + /** + * Assoc array of attribute name to definition object. + * @type HTMLPurifier_AttrDef[] + */ + public $info = array(); + + /** + * Constructs the info array. The meat of this class. + * @param HTMLPurifier_Config $config + */ + protected function doSetup($config) + { + $this->info['text-align'] = new HTMLPurifier_AttrDef_Enum( + array('left', 'right', 'center', 'justify'), + false + ); + + $border_style = + $this->info['border-bottom-style'] = + $this->info['border-right-style'] = + $this->info['border-left-style'] = + $this->info['border-top-style'] = new HTMLPurifier_AttrDef_Enum( + array( + 'none', + 'hidden', + 'dotted', + 'dashed', + 'solid', + 'double', + 'groove', + 'ridge', + 'inset', + 'outset' + ), + false + ); + + $this->info['border-style'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_style); + + $this->info['clear'] = new HTMLPurifier_AttrDef_Enum( + array('none', 'left', 'right', 'both'), + false + ); + $this->info['float'] = new HTMLPurifier_AttrDef_Enum( + array('none', 'left', 'right'), + false + ); + $this->info['font-style'] = new HTMLPurifier_AttrDef_Enum( + array('normal', 'italic', 'oblique'), + false + ); + $this->info['font-variant'] = new HTMLPurifier_AttrDef_Enum( + array('normal', 'small-caps'), + false + ); + + $uri_or_none = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('none')), + new HTMLPurifier_AttrDef_CSS_URI() + ) + ); + + $this->info['list-style-position'] = new HTMLPurifier_AttrDef_Enum( + array('inside', 'outside'), + false + ); + $this->info['list-style-type'] = new HTMLPurifier_AttrDef_Enum( + array( + 'disc', + 'circle', + 'square', + 'decimal', + 'lower-roman', + 'upper-roman', + 'lower-alpha', + 'upper-alpha', + 'none' + ), + false + ); + $this->info['list-style-image'] = $uri_or_none; + + $this->info['list-style'] = new HTMLPurifier_AttrDef_CSS_ListStyle($config); + + $this->info['text-transform'] = new HTMLPurifier_AttrDef_Enum( + array('capitalize', 'uppercase', 'lowercase', 'none'), + false + ); + $this->info['color'] = new HTMLPurifier_AttrDef_CSS_Color(); + + $this->info['background-image'] = $uri_or_none; + $this->info['background-repeat'] = new HTMLPurifier_AttrDef_Enum( + array('repeat', 'repeat-x', 'repeat-y', 'no-repeat') + ); + $this->info['background-attachment'] = new HTMLPurifier_AttrDef_Enum( + array('scroll', 'fixed') + ); + $this->info['background-position'] = new HTMLPurifier_AttrDef_CSS_BackgroundPosition(); + + $border_color = + $this->info['border-top-color'] = + $this->info['border-bottom-color'] = + $this->info['border-left-color'] = + $this->info['border-right-color'] = + $this->info['background-color'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('transparent')), + new HTMLPurifier_AttrDef_CSS_Color() + ) + ); + + $this->info['background'] = new HTMLPurifier_AttrDef_CSS_Background($config); + + $this->info['border-color'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_color); + + $border_width = + $this->info['border-top-width'] = + $this->info['border-bottom-width'] = + $this->info['border-left-width'] = + $this->info['border-right-width'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('thin', 'medium', 'thick')), + new HTMLPurifier_AttrDef_CSS_Length('0') //disallow negative + ) + ); + + $this->info['border-width'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_width); + + $this->info['letter-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('normal')), + new HTMLPurifier_AttrDef_CSS_Length() + ) + ); + + $this->info['word-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('normal')), + new HTMLPurifier_AttrDef_CSS_Length() + ) + ); + + $this->info['font-size'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum( + array( + 'xx-small', + 'x-small', + 'small', + 'medium', + 'large', + 'x-large', + 'xx-large', + 'larger', + 'smaller' + ) + ), + new HTMLPurifier_AttrDef_CSS_Percentage(), + new HTMLPurifier_AttrDef_CSS_Length() + ) + ); + + $this->info['line-height'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('normal')), + new HTMLPurifier_AttrDef_CSS_Number(true), // no negatives + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true) + ) + ); + + $margin = + $this->info['margin-top'] = + $this->info['margin-bottom'] = + $this->info['margin-left'] = + $this->info['margin-right'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage(), + new HTMLPurifier_AttrDef_Enum(array('auto')) + ) + ); + + $this->info['margin'] = new HTMLPurifier_AttrDef_CSS_Multiple($margin); + + // non-negative + $padding = + $this->info['padding-top'] = + $this->info['padding-bottom'] = + $this->info['padding-left'] = + $this->info['padding-right'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true) + ) + ); + + $this->info['padding'] = new HTMLPurifier_AttrDef_CSS_Multiple($padding); + + $this->info['text-indent'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage() + ) + ); + + $trusted_wh = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true), + new HTMLPurifier_AttrDef_Enum(array('auto')) + ) + ); + $max = $config->get('CSS.MaxImgLength'); + + $this->info['min-width'] = + $this->info['max-width'] = + $this->info['min-height'] = + $this->info['max-height'] = + $this->info['width'] = + $this->info['height'] = + $max === null ? + $trusted_wh : + new HTMLPurifier_AttrDef_Switch( + 'img', + // For img tags: + new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length('0', $max), + new HTMLPurifier_AttrDef_Enum(array('auto')) + ) + ), + // For everyone else: + $trusted_wh + ); + + $this->info['text-decoration'] = new HTMLPurifier_AttrDef_CSS_TextDecoration(); + + $this->info['font-family'] = new HTMLPurifier_AttrDef_CSS_FontFamily(); + + // this could use specialized code + $this->info['font-weight'] = new HTMLPurifier_AttrDef_Enum( + array( + 'normal', + 'bold', + 'bolder', + 'lighter', + '100', + '200', + '300', + '400', + '500', + '600', + '700', + '800', + '900' + ), + false + ); + + // MUST be called after other font properties, as it references + // a CSSDefinition object + $this->info['font'] = new HTMLPurifier_AttrDef_CSS_Font($config); + + // same here + $this->info['border'] = + $this->info['border-bottom'] = + $this->info['border-top'] = + $this->info['border-left'] = + $this->info['border-right'] = new HTMLPurifier_AttrDef_CSS_Border($config); + + $this->info['border-collapse'] = new HTMLPurifier_AttrDef_Enum( + array('collapse', 'separate') + ); + + $this->info['caption-side'] = new HTMLPurifier_AttrDef_Enum( + array('top', 'bottom') + ); + + $this->info['table-layout'] = new HTMLPurifier_AttrDef_Enum( + array('auto', 'fixed') + ); + + $this->info['vertical-align'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum( + array( + 'baseline', + 'sub', + 'super', + 'top', + 'text-top', + 'middle', + 'bottom', + 'text-bottom' + ) + ), + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage() + ) + ); + + $this->info['border-spacing'] = new HTMLPurifier_AttrDef_CSS_Multiple(new HTMLPurifier_AttrDef_CSS_Length(), 2); + + // These CSS properties don't work on many browsers, but we live + // in THE FUTURE! + $this->info['white-space'] = new HTMLPurifier_AttrDef_Enum( + array('nowrap', 'normal', 'pre', 'pre-wrap', 'pre-line') + ); + + if ($config->get('CSS.Proprietary')) { + $this->doSetupProprietary($config); + } + + if ($config->get('CSS.AllowTricky')) { + $this->doSetupTricky($config); + } + + if ($config->get('CSS.Trusted')) { + $this->doSetupTrusted($config); + } + + $allow_important = $config->get('CSS.AllowImportant'); + // wrap all attr-defs with decorator that handles !important + foreach ($this->info as $k => $v) { + $this->info[$k] = new HTMLPurifier_AttrDef_CSS_ImportantDecorator($v, $allow_important); + } + + $this->setupConfigStuff($config); + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetupProprietary($config) + { + // Internet Explorer only scrollbar colors + $this->info['scrollbar-arrow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-base-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-darkshadow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-face-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-highlight-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-shadow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + + // vendor specific prefixes of opacity + $this->info['-moz-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + $this->info['-khtml-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + + // only opacity, for now + $this->info['filter'] = new HTMLPurifier_AttrDef_CSS_Filter(); + + // more CSS3 + $this->info['page-break-after'] = + $this->info['page-break-before'] = new HTMLPurifier_AttrDef_Enum( + array( + 'auto', + 'always', + 'avoid', + 'left', + 'right' + ) + ); + $this->info['page-break-inside'] = new HTMLPurifier_AttrDef_Enum(array('auto', 'avoid')); + + $border_radius = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Percentage(true), // disallow negative + new HTMLPurifier_AttrDef_CSS_Length('0') // disallow negative + )); + + $this->info['border-top-left-radius'] = + $this->info['border-top-right-radius'] = + $this->info['border-bottom-right-radius'] = + $this->info['border-bottom-left-radius'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_radius, 2); + // TODO: support SLASH syntax + $this->info['border-radius'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_radius, 4); + + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetupTricky($config) + { + $this->info['display'] = new HTMLPurifier_AttrDef_Enum( + array( + 'inline', + 'block', + 'list-item', + 'run-in', + 'compact', + 'marker', + 'table', + 'inline-block', + 'inline-table', + 'table-row-group', + 'table-header-group', + 'table-footer-group', + 'table-row', + 'table-column-group', + 'table-column', + 'table-cell', + 'table-caption', + 'none' + ) + ); + $this->info['visibility'] = new HTMLPurifier_AttrDef_Enum( + array('visible', 'hidden', 'collapse') + ); + $this->info['overflow'] = new HTMLPurifier_AttrDef_Enum(array('visible', 'hidden', 'auto', 'scroll')); + $this->info['opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetupTrusted($config) + { + $this->info['position'] = new HTMLPurifier_AttrDef_Enum( + array('static', 'relative', 'absolute', 'fixed') + ); + $this->info['top'] = + $this->info['left'] = + $this->info['right'] = + $this->info['bottom'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage(), + new HTMLPurifier_AttrDef_Enum(array('auto')), + ) + ); + $this->info['z-index'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Integer(), + new HTMLPurifier_AttrDef_Enum(array('auto')), + ) + ); + } + + /** + * Performs extra config-based processing. Based off of + * HTMLPurifier_HTMLDefinition. + * @param HTMLPurifier_Config $config + * @todo Refactor duplicate elements into common class (probably using + * composition, not inheritance). + */ + protected function setupConfigStuff($config) + { + // setup allowed elements + $support = "(for information on implementing this, see the " . + "support forums) "; + $allowed_properties = $config->get('CSS.AllowedProperties'); + if ($allowed_properties !== null) { + foreach ($this->info as $name => $d) { + if (!isset($allowed_properties[$name])) { + unset($this->info[$name]); + } + unset($allowed_properties[$name]); + } + // emit errors + foreach ($allowed_properties as $name => $d) { + // :TODO: Is this htmlspecialchars() call really necessary? + $name = htmlspecialchars($name); + trigger_error("Style attribute '$name' is not supported $support", E_USER_WARNING); + } + } + + $forbidden_properties = $config->get('CSS.ForbiddenProperties'); + if ($forbidden_properties !== null) { + foreach ($this->info as $name => $d) { + if (isset($forbidden_properties[$name])) { + unset($this->info[$name]); + } + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef.php new file mode 100644 index 000000000..8eb17b82e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef.php @@ -0,0 +1,52 @@ +<?php + +/** + * Defines allowed child nodes and validates nodes against it. + */ +abstract class HTMLPurifier_ChildDef +{ + /** + * Type of child definition, usually right-most part of class name lowercase. + * Used occasionally in terms of context. + * @type string + */ + public $type; + + /** + * Indicates whether or not an empty array of children is okay. + * + * This is necessary for redundant checking when changes affecting + * a child node may cause a parent node to now be disallowed. + * @type bool + */ + public $allow_empty; + + /** + * Lookup array of all elements that this definition could possibly allow. + * @type array + */ + public $elements = array(); + + /** + * Get lookup of tag names that should not close this element automatically. + * All other elements will do so. + * @param HTMLPurifier_Config $config HTMLPurifier_Config object + * @return array + */ + public function getAllowedElements($config) + { + return $this->elements; + } + + /** + * Validates nodes according to definition and returns modification. + * + * @param HTMLPurifier_Node[] $children Array of HTMLPurifier_Node + * @param HTMLPurifier_Config $config HTMLPurifier_Config object + * @param HTMLPurifier_Context $context HTMLPurifier_Context object + * @return bool|array true to leave nodes as is, false to remove parent node, array of replacement children + */ + abstract public function validateChildren($children, $config, $context); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Chameleon.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Chameleon.php new file mode 100644 index 000000000..7439be26b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Chameleon.php @@ -0,0 +1,67 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Custom.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Custom.php new file mode 100644 index 000000000..128132e96 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Custom.php @@ -0,0 +1,102 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Empty.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Empty.php new file mode 100644 index 000000000..a8a6cbdd2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Empty.php @@ -0,0 +1,38 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/List.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/List.php new file mode 100644 index 000000000..5a53a4b49 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/List.php @@ -0,0 +1,92 @@ +<?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); + + /** + * @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 = false; + + 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 === false) { + $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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Optional.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Optional.php new file mode 100644 index 000000000..b9468063b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Optional.php @@ -0,0 +1,45 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Required.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Required.php new file mode 100644 index 000000000..0d1c8f5f3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Required.php @@ -0,0 +1,118 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/StrictBlockquote.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/StrictBlockquote.php new file mode 100644 index 000000000..3270a46e1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/StrictBlockquote.php @@ -0,0 +1,110 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Table.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Table.php new file mode 100644 index 000000000..cb6b3e6cd --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Table.php @@ -0,0 +1,224 @@ +<?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)) { + 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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Config.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Config.php new file mode 100644 index 000000000..69e6d2765 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Config.php @@ -0,0 +1,920 @@ +<?php + +/** + * Configuration object that triggers customizable behavior. + * + * @warning This class is strongly defined: that means that the class + * will fail if an undefined directive is retrieved or set. + * + * @note Many classes that could (although many times don't) use the + * configuration object make it a mandatory parameter. This is + * because a configuration object should always be forwarded, + * otherwise, you run the risk of missing a parameter and then + * being stumped when a configuration directive doesn't work. + * + * @todo Reconsider some of the public member variables + */ +class HTMLPurifier_Config +{ + + /** + * HTML Purifier's version + * @type string + */ + public $version = '4.9.2'; + + /** + * Whether or not to automatically finalize + * the object if a read operation is done. + * @type bool + */ + public $autoFinalize = true; + + // protected member variables + + /** + * Namespace indexed array of serials for specific namespaces. + * @see getSerial() for more info. + * @type string[] + */ + protected $serials = array(); + + /** + * Serial for entire configuration object. + * @type string + */ + protected $serial; + + /** + * Parser for variables. + * @type HTMLPurifier_VarParser_Flexible + */ + protected $parser = null; + + /** + * Reference HTMLPurifier_ConfigSchema for value checking. + * @type HTMLPurifier_ConfigSchema + * @note This is public for introspective purposes. Please don't + * abuse! + */ + public $def; + + /** + * Indexed array of definitions. + * @type HTMLPurifier_Definition[] + */ + protected $definitions; + + /** + * Whether or not config is finalized. + * @type bool + */ + protected $finalized = false; + + /** + * Property list containing configuration directives. + * @type array + */ + protected $plist; + + /** + * Whether or not a set is taking place due to an alias lookup. + * @type bool + */ + private $aliasMode; + + /** + * Set to false if you do not want line and file numbers in errors. + * (useful when unit testing). This will also compress some errors + * and exceptions. + * @type bool + */ + public $chatty = true; + + /** + * Current lock; only gets to this namespace are allowed. + * @type string + */ + private $lock; + + /** + * Constructor + * @param HTMLPurifier_ConfigSchema $definition ConfigSchema that defines + * what directives are allowed. + * @param HTMLPurifier_PropertyList $parent + */ + public function __construct($definition, $parent = null) + { + $parent = $parent ? $parent : $definition->defaultPlist; + $this->plist = new HTMLPurifier_PropertyList($parent); + $this->def = $definition; // keep a copy around for checking + $this->parser = new HTMLPurifier_VarParser_Flexible(); + } + + /** + * Convenience constructor that creates a config object based on a mixed var + * @param mixed $config Variable that defines the state of the config + * object. Can be: a HTMLPurifier_Config() object, + * an array of directives based on loadArray(), + * or a string filename of an ini file. + * @param HTMLPurifier_ConfigSchema $schema Schema object + * @return HTMLPurifier_Config Configured object + */ + public static function create($config, $schema = null) + { + if ($config instanceof HTMLPurifier_Config) { + // pass-through + return $config; + } + if (!$schema) { + $ret = HTMLPurifier_Config::createDefault(); + } else { + $ret = new HTMLPurifier_Config($schema); + } + if (is_string($config)) { + $ret->loadIni($config); + } elseif (is_array($config)) $ret->loadArray($config); + return $ret; + } + + /** + * Creates a new config object that inherits from a previous one. + * @param HTMLPurifier_Config $config Configuration object to inherit from. + * @return HTMLPurifier_Config object with $config as its parent. + */ + public static function inherit(HTMLPurifier_Config $config) + { + return new HTMLPurifier_Config($config->def, $config->plist); + } + + /** + * Convenience constructor that creates a default configuration object. + * @return HTMLPurifier_Config default object. + */ + public static function createDefault() + { + $definition = HTMLPurifier_ConfigSchema::instance(); + $config = new HTMLPurifier_Config($definition); + return $config; + } + + /** + * Retrieves a value from the configuration. + * + * @param string $key String key + * @param mixed $a + * + * @return mixed + */ + public function get($key, $a = null) + { + if ($a !== null) { + $this->triggerError( + "Using deprecated API: use \$config->get('$key.$a') instead", + E_USER_WARNING + ); + $key = "$key.$a"; + } + if (!$this->finalized) { + $this->autoFinalize(); + } + if (!isset($this->def->info[$key])) { + // can't add % due to SimpleTest bug + $this->triggerError( + 'Cannot retrieve value of undefined directive ' . htmlspecialchars($key), + E_USER_WARNING + ); + return; + } + if (isset($this->def->info[$key]->isAlias)) { + $d = $this->def->info[$key]; + $this->triggerError( + 'Cannot get value from aliased directive, use real name ' . $d->key, + E_USER_ERROR + ); + return; + } + if ($this->lock) { + list($ns) = explode('.', $key); + if ($ns !== $this->lock) { + $this->triggerError( + 'Cannot get value of namespace ' . $ns . ' when lock for ' . + $this->lock . + ' is active, this probably indicates a Definition setup method ' . + 'is accessing directives that are not within its namespace', + E_USER_ERROR + ); + return; + } + } + return $this->plist->get($key); + } + + /** + * Retrieves an array of directives to values from a given namespace + * + * @param string $namespace String namespace + * + * @return array + */ + public function getBatch($namespace) + { + if (!$this->finalized) { + $this->autoFinalize(); + } + $full = $this->getAll(); + if (!isset($full[$namespace])) { + $this->triggerError( + 'Cannot retrieve undefined namespace ' . + htmlspecialchars($namespace), + E_USER_WARNING + ); + return; + } + return $full[$namespace]; + } + + /** + * Returns a SHA-1 signature of a segment of the configuration object + * that uniquely identifies that particular configuration + * + * @param string $namespace Namespace to get serial for + * + * @return string + * @note Revision is handled specially and is removed from the batch + * before processing! + */ + public function getBatchSerial($namespace) + { + if (empty($this->serials[$namespace])) { + $batch = $this->getBatch($namespace); + unset($batch['DefinitionRev']); + $this->serials[$namespace] = sha1(serialize($batch)); + } + return $this->serials[$namespace]; + } + + /** + * Returns a SHA-1 signature for the entire configuration object + * that uniquely identifies that particular configuration + * + * @return string + */ + public function getSerial() + { + if (empty($this->serial)) { + $this->serial = sha1(serialize($this->getAll())); + } + return $this->serial; + } + + /** + * Retrieves all directives, organized by namespace + * + * @warning This is a pretty inefficient function, avoid if you can + */ + public function getAll() + { + if (!$this->finalized) { + $this->autoFinalize(); + } + $ret = array(); + foreach ($this->plist->squash() as $name => $value) { + list($ns, $key) = explode('.', $name, 2); + $ret[$ns][$key] = $value; + } + return $ret; + } + + /** + * Sets a value to configuration. + * + * @param string $key key + * @param mixed $value value + * @param mixed $a + */ + public function set($key, $value, $a = null) + { + if (strpos($key, '.') === false) { + $namespace = $key; + $directive = $value; + $value = $a; + $key = "$key.$directive"; + $this->triggerError("Using deprecated API: use \$config->set('$key', ...) instead", E_USER_NOTICE); + } else { + list($namespace) = explode('.', $key); + } + if ($this->isFinalized('Cannot set directive after finalization')) { + return; + } + if (!isset($this->def->info[$key])) { + $this->triggerError( + 'Cannot set undefined directive ' . htmlspecialchars($key) . ' to value', + E_USER_WARNING + ); + return; + } + $def = $this->def->info[$key]; + + if (isset($def->isAlias)) { + if ($this->aliasMode) { + $this->triggerError( + 'Double-aliases not allowed, please fix '. + 'ConfigSchema bug with' . $key, + E_USER_ERROR + ); + return; + } + $this->aliasMode = true; + $this->set($def->key, $value); + $this->aliasMode = false; + $this->triggerError("$key is an alias, preferred directive name is {$def->key}", E_USER_NOTICE); + return; + } + + // Raw type might be negative when using the fully optimized form + // of stdclass, which indicates allow_null == true + $rtype = is_int($def) ? $def : $def->type; + if ($rtype < 0) { + $type = -$rtype; + $allow_null = true; + } else { + $type = $rtype; + $allow_null = isset($def->allow_null); + } + + try { + $value = $this->parser->parse($value, $type, $allow_null); + } catch (HTMLPurifier_VarParserException $e) { + $this->triggerError( + 'Value for ' . $key . ' is of invalid type, should be ' . + HTMLPurifier_VarParser::getTypeName($type), + E_USER_WARNING + ); + return; + } + if (is_string($value) && is_object($def)) { + // resolve value alias if defined + if (isset($def->aliases[$value])) { + $value = $def->aliases[$value]; + } + // check to see if the value is allowed + if (isset($def->allowed) && !isset($def->allowed[$value])) { + $this->triggerError( + 'Value not supported, valid values are: ' . + $this->_listify($def->allowed), + E_USER_WARNING + ); + return; + } + } + $this->plist->set($key, $value); + + // reset definitions if the directives they depend on changed + // this is a very costly process, so it's discouraged + // with finalization + if ($namespace == 'HTML' || $namespace == 'CSS' || $namespace == 'URI') { + $this->definitions[$namespace] = null; + } + + $this->serials[$namespace] = false; + } + + /** + * Convenience function for error reporting + * + * @param array $lookup + * + * @return string + */ + private function _listify($lookup) + { + $list = array(); + foreach ($lookup as $name => $b) { + $list[] = $name; + } + return implode(', ', $list); + } + + /** + * Retrieves object reference to the HTML definition. + * + * @param bool $raw Return a copy that has not been setup yet. Must be + * called before it's been setup, otherwise won't work. + * @param bool $optimized If true, this method may return null, to + * indicate that a cached version of the modified + * definition object is available and no further edits + * are necessary. Consider using + * maybeGetRawHTMLDefinition, which is more explicitly + * named, instead. + * + * @return HTMLPurifier_HTMLDefinition + */ + public function getHTMLDefinition($raw = false, $optimized = false) + { + return $this->getDefinition('HTML', $raw, $optimized); + } + + /** + * Retrieves object reference to the CSS definition + * + * @param bool $raw Return a copy that has not been setup yet. Must be + * called before it's been setup, otherwise won't work. + * @param bool $optimized If true, this method may return null, to + * indicate that a cached version of the modified + * definition object is available and no further edits + * are necessary. Consider using + * maybeGetRawCSSDefinition, which is more explicitly + * named, instead. + * + * @return HTMLPurifier_CSSDefinition + */ + public function getCSSDefinition($raw = false, $optimized = false) + { + return $this->getDefinition('CSS', $raw, $optimized); + } + + /** + * Retrieves object reference to the URI definition + * + * @param bool $raw Return a copy that has not been setup yet. Must be + * called before it's been setup, otherwise won't work. + * @param bool $optimized If true, this method may return null, to + * indicate that a cached version of the modified + * definition object is available and no further edits + * are necessary. Consider using + * maybeGetRawURIDefinition, which is more explicitly + * named, instead. + * + * @return HTMLPurifier_URIDefinition + */ + public function getURIDefinition($raw = false, $optimized = false) + { + return $this->getDefinition('URI', $raw, $optimized); + } + + /** + * Retrieves a definition + * + * @param string $type Type of definition: HTML, CSS, etc + * @param bool $raw Whether or not definition should be returned raw + * @param bool $optimized Only has an effect when $raw is true. Whether + * or not to return null if the result is already present in + * the cache. This is off by default for backwards + * compatibility reasons, but you need to do things this + * way in order to ensure that caching is done properly. + * Check out enduser-customize.html for more details. + * We probably won't ever change this default, as much as the + * maybe semantics is the "right thing to do." + * + * @throws HTMLPurifier_Exception + * @return HTMLPurifier_Definition + */ + public function getDefinition($type, $raw = false, $optimized = false) + { + if ($optimized && !$raw) { + throw new HTMLPurifier_Exception("Cannot set optimized = true when raw = false"); + } + if (!$this->finalized) { + $this->autoFinalize(); + } + // temporarily suspend locks, so we can handle recursive definition calls + $lock = $this->lock; + $this->lock = null; + $factory = HTMLPurifier_DefinitionCacheFactory::instance(); + $cache = $factory->create($type, $this); + $this->lock = $lock; + if (!$raw) { + // full definition + // --------------- + // check if definition is in memory + if (!empty($this->definitions[$type])) { + $def = $this->definitions[$type]; + // check if the definition is setup + if ($def->setup) { + return $def; + } else { + $def->setup($this); + if ($def->optimized) { + $cache->add($def, $this); + } + return $def; + } + } + // check if definition is in cache + $def = $cache->get($this); + if ($def) { + // definition in cache, save to memory and return it + $this->definitions[$type] = $def; + return $def; + } + // initialize it + $def = $this->initDefinition($type); + // set it up + $this->lock = $type; + $def->setup($this); + $this->lock = null; + // save in cache + $cache->add($def, $this); + // return it + return $def; + } else { + // raw definition + // -------------- + // check preconditions + $def = null; + if ($optimized) { + if (is_null($this->get($type . '.DefinitionID'))) { + // fatally error out if definition ID not set + throw new HTMLPurifier_Exception( + "Cannot retrieve raw version without specifying %$type.DefinitionID" + ); + } + } + if (!empty($this->definitions[$type])) { + $def = $this->definitions[$type]; + if ($def->setup && !$optimized) { + $extra = $this->chatty ? + " (try moving this code block earlier in your initialization)" : + ""; + throw new HTMLPurifier_Exception( + "Cannot retrieve raw definition after it has already been setup" . + $extra + ); + } + if ($def->optimized === null) { + $extra = $this->chatty ? " (try flushing your cache)" : ""; + throw new HTMLPurifier_Exception( + "Optimization status of definition is unknown" . $extra + ); + } + if ($def->optimized !== $optimized) { + $msg = $optimized ? "optimized" : "unoptimized"; + $extra = $this->chatty ? + " (this backtrace is for the first inconsistent call, which was for a $msg raw definition)" + : ""; + throw new HTMLPurifier_Exception( + "Inconsistent use of optimized and unoptimized raw definition retrievals" . $extra + ); + } + } + // check if definition was in memory + if ($def) { + if ($def->setup) { + // invariant: $optimized === true (checked above) + return null; + } else { + return $def; + } + } + // if optimized, check if definition was in cache + // (because we do the memory check first, this formulation + // is prone to cache slamming, but I think + // guaranteeing that either /all/ of the raw + // setup code or /none/ of it is run is more important.) + if ($optimized) { + // This code path only gets run once; once we put + // something in $definitions (which is guaranteed by the + // trailing code), we always short-circuit above. + $def = $cache->get($this); + if ($def) { + // save the full definition for later, but don't + // return it yet + $this->definitions[$type] = $def; + return null; + } + } + // check invariants for creation + if (!$optimized) { + if (!is_null($this->get($type . '.DefinitionID'))) { + if ($this->chatty) { + $this->triggerError( + 'Due to a documentation error in previous version of HTML Purifier, your ' . + 'definitions are not being cached. If this is OK, you can remove the ' . + '%$type.DefinitionRev and %$type.DefinitionID declaration. Otherwise, ' . + 'modify your code to use maybeGetRawDefinition, and test if the returned ' . + 'value is null before making any edits (if it is null, that means that a ' . + 'cached version is available, and no raw operations are necessary). See ' . + '<a href="http://htmlpurifier.org/docs/enduser-customize.html#optimized">' . + 'Customize</a> for more details', + E_USER_WARNING + ); + } else { + $this->triggerError( + "Useless DefinitionID declaration", + E_USER_WARNING + ); + } + } + } + // initialize it + $def = $this->initDefinition($type); + $def->optimized = $optimized; + return $def; + } + throw new HTMLPurifier_Exception("The impossible happened!"); + } + + /** + * Initialise definition + * + * @param string $type What type of definition to create + * + * @return HTMLPurifier_CSSDefinition|HTMLPurifier_HTMLDefinition|HTMLPurifier_URIDefinition + * @throws HTMLPurifier_Exception + */ + private function initDefinition($type) + { + // quick checks failed, let's create the object + if ($type == 'HTML') { + $def = new HTMLPurifier_HTMLDefinition(); + } elseif ($type == 'CSS') { + $def = new HTMLPurifier_CSSDefinition(); + } elseif ($type == 'URI') { + $def = new HTMLPurifier_URIDefinition(); + } else { + throw new HTMLPurifier_Exception( + "Definition of $type type not supported" + ); + } + $this->definitions[$type] = $def; + return $def; + } + + public function maybeGetRawDefinition($name) + { + return $this->getDefinition($name, true, true); + } + + /** + * @return HTMLPurifier_HTMLDefinition + */ + public function maybeGetRawHTMLDefinition() + { + return $this->getDefinition('HTML', true, true); + } + + /** + * @return HTMLPurifier_CSSDefinition + */ + public function maybeGetRawCSSDefinition() + { + return $this->getDefinition('CSS', true, true); + } + + /** + * @return HTMLPurifier_URIDefinition + */ + public function maybeGetRawURIDefinition() + { + return $this->getDefinition('URI', true, true); + } + + /** + * Loads configuration values from an array with the following structure: + * Namespace.Directive => Value + * + * @param array $config_array Configuration associative array + */ + public function loadArray($config_array) + { + if ($this->isFinalized('Cannot load directives after finalization')) { + return; + } + foreach ($config_array as $key => $value) { + $key = str_replace('_', '.', $key); + if (strpos($key, '.') !== false) { + $this->set($key, $value); + } else { + $namespace = $key; + $namespace_values = $value; + foreach ($namespace_values as $directive => $value2) { + $this->set($namespace .'.'. $directive, $value2); + } + } + } + } + + /** + * Returns a list of array(namespace, directive) for all directives + * that are allowed in a web-form context as per an allowed + * namespaces/directives list. + * + * @param array $allowed List of allowed namespaces/directives + * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy + * + * @return array + */ + public static function getAllowedDirectivesForForm($allowed, $schema = null) + { + if (!$schema) { + $schema = HTMLPurifier_ConfigSchema::instance(); + } + if ($allowed !== true) { + if (is_string($allowed)) { + $allowed = array($allowed); + } + $allowed_ns = array(); + $allowed_directives = array(); + $blacklisted_directives = array(); + foreach ($allowed as $ns_or_directive) { + if (strpos($ns_or_directive, '.') !== false) { + // directive + if ($ns_or_directive[0] == '-') { + $blacklisted_directives[substr($ns_or_directive, 1)] = true; + } else { + $allowed_directives[$ns_or_directive] = true; + } + } else { + // namespace + $allowed_ns[$ns_or_directive] = true; + } + } + } + $ret = array(); + foreach ($schema->info as $key => $def) { + list($ns, $directive) = explode('.', $key, 2); + if ($allowed !== true) { + if (isset($blacklisted_directives["$ns.$directive"])) { + continue; + } + if (!isset($allowed_directives["$ns.$directive"]) && !isset($allowed_ns[$ns])) { + continue; + } + } + if (isset($def->isAlias)) { + continue; + } + if ($directive == 'DefinitionID' || $directive == 'DefinitionRev') { + continue; + } + $ret[] = array($ns, $directive); + } + return $ret; + } + + /** + * Loads configuration values from $_GET/$_POST that were posted + * via ConfigForm + * + * @param array $array $_GET or $_POST array to import + * @param string|bool $index Index/name that the config variables are in + * @param array|bool $allowed List of allowed namespaces/directives + * @param bool $mq_fix Boolean whether or not to enable magic quotes fix + * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy + * + * @return mixed + */ + public static function loadArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) + { + $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $schema); + $config = HTMLPurifier_Config::create($ret, $schema); + return $config; + } + + /** + * Merges in configuration values from $_GET/$_POST to object. NOT STATIC. + * + * @param array $array $_GET or $_POST array to import + * @param string|bool $index Index/name that the config variables are in + * @param array|bool $allowed List of allowed namespaces/directives + * @param bool $mq_fix Boolean whether or not to enable magic quotes fix + */ + public function mergeArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true) + { + $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $this->def); + $this->loadArray($ret); + } + + /** + * Prepares an array from a form into something usable for the more + * strict parts of HTMLPurifier_Config + * + * @param array $array $_GET or $_POST array to import + * @param string|bool $index Index/name that the config variables are in + * @param array|bool $allowed List of allowed namespaces/directives + * @param bool $mq_fix Boolean whether or not to enable magic quotes fix + * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy + * + * @return array + */ + public static function prepareArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) + { + if ($index !== false) { + $array = (isset($array[$index]) && is_array($array[$index])) ? $array[$index] : array(); + } + $mq = $mq_fix && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc(); + + $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $schema); + $ret = array(); + foreach ($allowed as $key) { + list($ns, $directive) = $key; + $skey = "$ns.$directive"; + if (!empty($array["Null_$skey"])) { + $ret[$ns][$directive] = null; + continue; + } + if (!isset($array[$skey])) { + continue; + } + $value = $mq ? stripslashes($array[$skey]) : $array[$skey]; + $ret[$ns][$directive] = $value; + } + return $ret; + } + + /** + * Loads configuration values from an ini file + * + * @param string $filename Name of ini file + */ + public function loadIni($filename) + { + if ($this->isFinalized('Cannot load directives after finalization')) { + return; + } + $array = parse_ini_file($filename, true); + $this->loadArray($array); + } + + /** + * Checks whether or not the configuration object is finalized. + * + * @param string|bool $error String error message, or false for no error + * + * @return bool + */ + public function isFinalized($error = false) + { + if ($this->finalized && $error) { + $this->triggerError($error, E_USER_ERROR); + } + return $this->finalized; + } + + /** + * Finalizes configuration only if auto finalize is on and not + * already finalized + */ + public function autoFinalize() + { + if ($this->autoFinalize) { + $this->finalize(); + } else { + $this->plist->squash(true); + } + } + + /** + * Finalizes a configuration object, prohibiting further change + */ + public function finalize() + { + $this->finalized = true; + $this->parser = null; + } + + /** + * Produces a nicely formatted error message by supplying the + * stack frame information OUTSIDE of HTMLPurifier_Config. + * + * @param string $msg An error message + * @param int $no An error number + */ + protected function triggerError($msg, $no) + { + // determine previous stack frame + $extra = ''; + if ($this->chatty) { + $trace = debug_backtrace(); + // zip(tail(trace), trace) -- but PHP is not Haskell har har + for ($i = 0, $c = count($trace); $i < $c - 1; $i++) { + // XXX this is not correct on some versions of HTML Purifier + if ($trace[$i + 1]['class'] === 'HTMLPurifier_Config') { + continue; + } + $frame = $trace[$i]; + $extra = " invoked on line {$frame['line']} in file {$frame['file']}"; + break; + } + } + trigger_error($msg . $extra, $no); + } + + /** + * Returns a serialized form of the configuration object that can + * be reconstituted. + * + * @return string + */ + public function serialize() + { + $this->getDefinition('HTML'); + $this->getDefinition('CSS'); + $this->getDefinition('URI'); + return serialize($this); + } + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema.php new file mode 100644 index 000000000..bfbb0f92f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema.php @@ -0,0 +1,176 @@ +<?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_DirectiveDef::$type 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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php new file mode 100644 index 000000000..d5906cd46 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php @@ -0,0 +1,48 @@ +<?php + +/** + * Converts HTMLPurifier_ConfigSchema_Interchange to our runtime + * representation used to perform checks on user configuration. + */ +class HTMLPurifier_ConfigSchema_Builder_ConfigSchema +{ + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @return HTMLPurifier_ConfigSchema + */ + public function build($interchange) + { + $schema = new HTMLPurifier_ConfigSchema(); + foreach ($interchange->directives as $d) { + $schema->add( + $d->id->key, + $d->default, + $d->type, + $d->typeAllowsNull + ); + if ($d->allowed !== null) { + $schema->addAllowedValues( + $d->id->key, + $d->allowed + ); + } + foreach ($d->aliases as $alias) { + $schema->addAlias( + $alias->key, + $d->id->key + ); + } + if ($d->valueAliases !== null) { + $schema->addValueAliases( + $d->id->key, + $d->valueAliases + ); + } + } + $schema->postProcess(); + return $schema; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/Xml.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/Xml.php new file mode 100644 index 000000000..5fa56f7dd --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/Xml.php @@ -0,0 +1,144 @@ +<?php + +/** + * Converts HTMLPurifier_ConfigSchema_Interchange to an XML format, + * which can be further processed to generate documentation. + */ +class HTMLPurifier_ConfigSchema_Builder_Xml extends XMLWriter +{ + + /** + * @type HTMLPurifier_ConfigSchema_Interchange + */ + protected $interchange; + + /** + * @type string + */ + private $namespace; + + /** + * @param string $html + */ + protected function writeHTMLDiv($html) + { + $this->startElement('div'); + + $purifier = HTMLPurifier::getInstance(); + $html = $purifier->purify($html); + $this->writeAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + $this->writeRaw($html); + + $this->endElement(); // div + } + + /** + * @param mixed $var + * @return string + */ + protected function export($var) + { + if ($var === array()) { + return 'array()'; + } + return var_export($var, true); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + */ + public function build($interchange) + { + // global access, only use as last resort + $this->interchange = $interchange; + + $this->setIndent(true); + $this->startDocument('1.0', 'UTF-8'); + $this->startElement('configdoc'); + $this->writeElement('title', $interchange->name); + + foreach ($interchange->directives as $directive) { + $this->buildDirective($directive); + } + + if ($this->namespace) { + $this->endElement(); + } // namespace + + $this->endElement(); // configdoc + $this->flush(); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive + */ + public function buildDirective($directive) + { + // Kludge, although I suppose having a notion of a "root namespace" + // certainly makes things look nicer when documentation is built. + // Depends on things being sorted. + if (!$this->namespace || $this->namespace !== $directive->id->getRootNamespace()) { + if ($this->namespace) { + $this->endElement(); + } // namespace + $this->namespace = $directive->id->getRootNamespace(); + $this->startElement('namespace'); + $this->writeAttribute('id', $this->namespace); + $this->writeElement('name', $this->namespace); + } + + $this->startElement('directive'); + $this->writeAttribute('id', $directive->id->toString()); + + $this->writeElement('name', $directive->id->getDirective()); + + $this->startElement('aliases'); + foreach ($directive->aliases as $alias) { + $this->writeElement('alias', $alias->toString()); + } + $this->endElement(); // aliases + + $this->startElement('constraints'); + if ($directive->version) { + $this->writeElement('version', $directive->version); + } + $this->startElement('type'); + if ($directive->typeAllowsNull) { + $this->writeAttribute('allow-null', 'yes'); + } + $this->text($directive->type); + $this->endElement(); // type + if ($directive->allowed) { + $this->startElement('allowed'); + foreach ($directive->allowed as $value => $x) { + $this->writeElement('value', $value); + } + $this->endElement(); // allowed + } + $this->writeElement('default', $this->export($directive->default)); + $this->writeAttribute('xml:space', 'preserve'); + if ($directive->external) { + $this->startElement('external'); + foreach ($directive->external as $project) { + $this->writeElement('project', $project); + } + $this->endElement(); + } + $this->endElement(); // constraints + + if ($directive->deprecatedVersion) { + $this->startElement('deprecated'); + $this->writeElement('version', $directive->deprecatedVersion); + $this->writeElement('use', $directive->deprecatedUse->toString()); + $this->endElement(); // deprecated + } + + $this->startElement('description'); + $this->writeHTMLDiv($directive->description); + $this->endElement(); // description + + $this->endElement(); // directive + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Exception.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Exception.php new file mode 100644 index 000000000..2671516c5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Exception.php @@ -0,0 +1,11 @@ +<?php + +/** + * Exceptions related to configuration schema + */ +class HTMLPurifier_ConfigSchema_Exception extends HTMLPurifier_Exception +{ + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange.php new file mode 100644 index 000000000..0e08ae8fe --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange.php @@ -0,0 +1,47 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Directive.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Directive.php new file mode 100644 index 000000000..127a39a67 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Directive.php @@ -0,0 +1,89 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Id.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Id.php new file mode 100644 index 000000000..126f09d95 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Id.php @@ -0,0 +1,58 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/InterchangeBuilder.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/InterchangeBuilder.php new file mode 100644 index 000000000..655e6dd1b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/InterchangeBuilder.php @@ -0,0 +1,226 @@ +<?php + +class HTMLPurifier_ConfigSchema_InterchangeBuilder +{ + + /** + * Used for processing DEFAULT, nothing else. + * @type HTMLPurifier_VarParser + */ + protected $varParser; + + /** + * @param HTMLPurifier_VarParser $varParser + */ + public function __construct($varParser = null) + { + $this->varParser = $varParser ? $varParser : new HTMLPurifier_VarParser_Native(); + } + + /** + * @param string $dir + * @return HTMLPurifier_ConfigSchema_Interchange + */ + public static function buildFromDirectory($dir = null) + { + $builder = new HTMLPurifier_ConfigSchema_InterchangeBuilder(); + $interchange = new HTMLPurifier_ConfigSchema_Interchange(); + return $builder->buildDir($interchange, $dir); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @param string $dir + * @return HTMLPurifier_ConfigSchema_Interchange + */ + public function buildDir($interchange, $dir = null) + { + if (!$dir) { + $dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema'; + } + if (file_exists($dir . '/info.ini')) { + $info = parse_ini_file($dir . '/info.ini'); + $interchange->name = $info['name']; + } + + $files = array(); + $dh = opendir($dir); + while (false !== ($file = readdir($dh))) { + if (!$file || $file[0] == '.' || strrchr($file, '.') !== '.txt') { + continue; + } + $files[] = $file; + } + closedir($dh); + + sort($files); + foreach ($files as $file) { + $this->buildFile($interchange, $dir . '/' . $file); + } + return $interchange; + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @param string $file + */ + public function buildFile($interchange, $file) + { + $parser = new HTMLPurifier_StringHashParser(); + $this->build( + $interchange, + new HTMLPurifier_StringHash($parser->parseFile($file)) + ); + } + + /** + * Builds an interchange object based on a hash. + * @param HTMLPurifier_ConfigSchema_Interchange $interchange HTMLPurifier_ConfigSchema_Interchange object to build + * @param HTMLPurifier_StringHash $hash source data + * @throws HTMLPurifier_ConfigSchema_Exception + */ + public function build($interchange, $hash) + { + if (!$hash instanceof HTMLPurifier_StringHash) { + $hash = new HTMLPurifier_StringHash($hash); + } + if (!isset($hash['ID'])) { + throw new HTMLPurifier_ConfigSchema_Exception('Hash does not have any ID'); + } + if (strpos($hash['ID'], '.') === false) { + if (count($hash) == 2 && isset($hash['DESCRIPTION'])) { + $hash->offsetGet('DESCRIPTION'); // prevent complaining + } else { + throw new HTMLPurifier_ConfigSchema_Exception('All directives must have a namespace'); + } + } else { + $this->buildDirective($interchange, $hash); + } + $this->_findUnused($hash); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @param HTMLPurifier_StringHash $hash + * @throws HTMLPurifier_ConfigSchema_Exception + */ + public function buildDirective($interchange, $hash) + { + $directive = new HTMLPurifier_ConfigSchema_Interchange_Directive(); + + // These are required elements: + $directive->id = $this->id($hash->offsetGet('ID')); + $id = $directive->id->toString(); // convenience + + if (isset($hash['TYPE'])) { + $type = explode('/', $hash->offsetGet('TYPE')); + if (isset($type[1])) { + $directive->typeAllowsNull = true; + } + $directive->type = $type[0]; + } else { + throw new HTMLPurifier_ConfigSchema_Exception("TYPE in directive hash '$id' not defined"); + } + + if (isset($hash['DEFAULT'])) { + try { + $directive->default = $this->varParser->parse( + $hash->offsetGet('DEFAULT'), + $directive->type, + $directive->typeAllowsNull + ); + } catch (HTMLPurifier_VarParserException $e) { + throw new HTMLPurifier_ConfigSchema_Exception($e->getMessage() . " in DEFAULT in directive hash '$id'"); + } + } + + if (isset($hash['DESCRIPTION'])) { + $directive->description = $hash->offsetGet('DESCRIPTION'); + } + + if (isset($hash['ALLOWED'])) { + $directive->allowed = $this->lookup($this->evalArray($hash->offsetGet('ALLOWED'))); + } + + if (isset($hash['VALUE-ALIASES'])) { + $directive->valueAliases = $this->evalArray($hash->offsetGet('VALUE-ALIASES')); + } + + if (isset($hash['ALIASES'])) { + $raw_aliases = trim($hash->offsetGet('ALIASES')); + $aliases = preg_split('/\s*,\s*/', $raw_aliases); + foreach ($aliases as $alias) { + $directive->aliases[] = $this->id($alias); + } + } + + if (isset($hash['VERSION'])) { + $directive->version = $hash->offsetGet('VERSION'); + } + + if (isset($hash['DEPRECATED-USE'])) { + $directive->deprecatedUse = $this->id($hash->offsetGet('DEPRECATED-USE')); + } + + if (isset($hash['DEPRECATED-VERSION'])) { + $directive->deprecatedVersion = $hash->offsetGet('DEPRECATED-VERSION'); + } + + if (isset($hash['EXTERNAL'])) { + $directive->external = preg_split('/\s*,\s*/', trim($hash->offsetGet('EXTERNAL'))); + } + + $interchange->addDirective($directive); + } + + /** + * Evaluates an array PHP code string without array() wrapper + * @param string $contents + */ + protected function evalArray($contents) + { + return eval('return array(' . $contents . ');'); + } + + /** + * Converts an array list into a lookup array. + * @param array $array + * @return array + */ + protected function lookup($array) + { + $ret = array(); + foreach ($array as $val) { + $ret[$val] = true; + } + return $ret; + } + + /** + * Convenience function that creates an HTMLPurifier_ConfigSchema_Interchange_Id + * object based on a string Id. + * @param string $id + * @return HTMLPurifier_ConfigSchema_Interchange_Id + */ + protected function id($id) + { + return HTMLPurifier_ConfigSchema_Interchange_Id::make($id); + } + + /** + * Triggers errors for any unused keys passed in the hash; such keys + * may indicate typos, missing values, etc. + * @param HTMLPurifier_StringHash $hash Hash to check. + */ + protected function _findUnused($hash) + { + $accessed = $hash->getAccessed(); + foreach ($hash as $k => $v) { + if (!isset($accessed[$k])) { + trigger_error("String hash key '$k' not used by builder", E_USER_NOTICE); + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Validator.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Validator.php new file mode 100644 index 000000000..fb3127788 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Validator.php @@ -0,0 +1,248 @@ +<?php + +/** + * Performs validations on HTMLPurifier_ConfigSchema_Interchange + * + * @note If you see '// handled by InterchangeBuilder', that means a + * design decision in that class would prevent this validation from + * ever being necessary. We have them anyway, however, for + * redundancy. + */ +class HTMLPurifier_ConfigSchema_Validator +{ + + /** + * @type HTMLPurifier_ConfigSchema_Interchange + */ + protected $interchange; + + /** + * @type array + */ + protected $aliases; + + /** + * Context-stack to provide easy to read error messages. + * @type array + */ + protected $context = array(); + + /** + * to test default's type. + * @type HTMLPurifier_VarParser + */ + protected $parser; + + public function __construct() + { + $this->parser = new HTMLPurifier_VarParser(); + } + + /** + * Validates a fully-formed interchange object. + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @return bool + */ + public function validate($interchange) + { + $this->interchange = $interchange; + $this->aliases = array(); + // PHP is a bit lax with integer <=> string conversions in + // arrays, so we don't use the identical !== comparison + foreach ($interchange->directives as $i => $directive) { + $id = $directive->id->toString(); + if ($i != $id) { + $this->error(false, "Integrity violation: key '$i' does not match internal id '$id'"); + } + $this->validateDirective($directive); + } + return true; + } + + /** + * Validates a HTMLPurifier_ConfigSchema_Interchange_Id object. + * @param HTMLPurifier_ConfigSchema_Interchange_Id $id + */ + public function validateId($id) + { + $id_string = $id->toString(); + $this->context[] = "id '$id_string'"; + if (!$id instanceof HTMLPurifier_ConfigSchema_Interchange_Id) { + // handled by InterchangeBuilder + $this->error(false, 'is not an instance of HTMLPurifier_ConfigSchema_Interchange_Id'); + } + // keys are now unconstrained (we might want to narrow down to A-Za-z0-9.) + // we probably should check that it has at least one namespace + $this->with($id, 'key') + ->assertNotEmpty() + ->assertIsString(); // implicit assertIsString handled by InterchangeBuilder + array_pop($this->context); + } + + /** + * Validates a HTMLPurifier_ConfigSchema_Interchange_Directive object. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirective($d) + { + $id = $d->id->toString(); + $this->context[] = "directive '$id'"; + $this->validateId($d->id); + + $this->with($d, 'description') + ->assertNotEmpty(); + + // BEGIN - handled by InterchangeBuilder + $this->with($d, 'type') + ->assertNotEmpty(); + $this->with($d, 'typeAllowsNull') + ->assertIsBool(); + try { + // This also tests validity of $d->type + $this->parser->parse($d->default, $d->type, $d->typeAllowsNull); + } catch (HTMLPurifier_VarParserException $e) { + $this->error('default', 'had error: ' . $e->getMessage()); + } + // END - handled by InterchangeBuilder + + if (!is_null($d->allowed) || !empty($d->valueAliases)) { + // allowed and valueAliases require that we be dealing with + // strings, so check for that early. + $d_int = HTMLPurifier_VarParser::$types[$d->type]; + if (!isset(HTMLPurifier_VarParser::$stringTypes[$d_int])) { + $this->error('type', 'must be a string type when used with allowed or value aliases'); + } + } + + $this->validateDirectiveAllowed($d); + $this->validateDirectiveValueAliases($d); + $this->validateDirectiveAliases($d); + + array_pop($this->context); + } + + /** + * Extra validation if $allowed member variable of + * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirectiveAllowed($d) + { + if (is_null($d->allowed)) { + return; + } + $this->with($d, 'allowed') + ->assertNotEmpty() + ->assertIsLookup(); // handled by InterchangeBuilder + if (is_string($d->default) && !isset($d->allowed[$d->default])) { + $this->error('default', 'must be an allowed value'); + } + $this->context[] = 'allowed'; + foreach ($d->allowed as $val => $x) { + if (!is_string($val)) { + $this->error("value $val", 'must be a string'); + } + } + array_pop($this->context); + } + + /** + * Extra validation if $valueAliases member variable of + * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirectiveValueAliases($d) + { + if (is_null($d->valueAliases)) { + return; + } + $this->with($d, 'valueAliases') + ->assertIsArray(); // handled by InterchangeBuilder + $this->context[] = 'valueAliases'; + foreach ($d->valueAliases as $alias => $real) { + if (!is_string($alias)) { + $this->error("alias $alias", 'must be a string'); + } + if (!is_string($real)) { + $this->error("alias target $real from alias '$alias'", 'must be a string'); + } + if ($alias === $real) { + $this->error("alias '$alias'", "must not be an alias to itself"); + } + } + if (!is_null($d->allowed)) { + foreach ($d->valueAliases as $alias => $real) { + if (isset($d->allowed[$alias])) { + $this->error("alias '$alias'", 'must not be an allowed value'); + } elseif (!isset($d->allowed[$real])) { + $this->error("alias '$alias'", 'must be an alias to an allowed value'); + } + } + } + array_pop($this->context); + } + + /** + * Extra validation if $aliases member variable of + * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirectiveAliases($d) + { + $this->with($d, 'aliases') + ->assertIsArray(); // handled by InterchangeBuilder + $this->context[] = 'aliases'; + foreach ($d->aliases as $alias) { + $this->validateId($alias); + $s = $alias->toString(); + if (isset($this->interchange->directives[$s])) { + $this->error("alias '$s'", 'collides with another directive'); + } + if (isset($this->aliases[$s])) { + $other_directive = $this->aliases[$s]; + $this->error("alias '$s'", "collides with alias for directive '$other_directive'"); + } + $this->aliases[$s] = $d->id->toString(); + } + array_pop($this->context); + } + + // protected helper functions + + /** + * Convenience function for generating HTMLPurifier_ConfigSchema_ValidatorAtom + * for validating simple member variables of objects. + * @param $obj + * @param $member + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + protected function with($obj, $member) + { + return new HTMLPurifier_ConfigSchema_ValidatorAtom($this->getFormattedContext(), $obj, $member); + } + + /** + * Emits an error, providing helpful context. + * @throws HTMLPurifier_ConfigSchema_Exception + */ + protected function error($target, $msg) + { + if ($target !== false) { + $prefix = ucfirst($target) . ' in ' . $this->getFormattedContext(); + } else { + $prefix = ucfirst($this->getFormattedContext()); + } + throw new HTMLPurifier_ConfigSchema_Exception(trim($prefix . ' ' . $msg)); + } + + /** + * Returns a formatted context string. + * @return string + */ + protected function getFormattedContext() + { + return implode(' in ', array_reverse($this->context)); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/ValidatorAtom.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/ValidatorAtom.php new file mode 100644 index 000000000..c9aa3644a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/ValidatorAtom.php @@ -0,0 +1,130 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema.ser b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema.ser Binary files differnew file mode 100644 index 000000000..371e948f1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema.ser diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt new file mode 100644 index 000000000..0517fed0a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt @@ -0,0 +1,8 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt new file mode 100644 index 000000000..249edd647 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt @@ -0,0 +1,12 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt new file mode 100644 index 000000000..9a8fa6a2e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt @@ -0,0 +1,9 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt new file mode 100644 index 000000000..b01788348 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt @@ -0,0 +1,9 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt new file mode 100644 index 000000000..e774b823b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt @@ -0,0 +1,19 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt new file mode 100644 index 000000000..533165e17 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt @@ -0,0 +1,11 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt new file mode 100644 index 000000000..9eb7e3846 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt @@ -0,0 +1,9 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt new file mode 100644 index 000000000..2f17bf477 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt @@ -0,0 +1,8 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt new file mode 100644 index 000000000..52654b53a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt @@ -0,0 +1,10 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt new file mode 100644 index 000000000..6440d2103 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt @@ -0,0 +1,16 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt new file mode 100644 index 000000000..f31d226f5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt @@ -0,0 +1,8 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt new file mode 100644 index 000000000..735d4b7a1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt @@ -0,0 +1,10 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt new file mode 100644 index 000000000..5f2b5e3d2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt @@ -0,0 +1,5 @@ +Attr.IDBlacklist +TYPE: list +DEFAULT: array() +DESCRIPTION: Array of IDs not allowed in the document. +--# vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt new file mode 100644 index 000000000..6f5824586 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt @@ -0,0 +1,9 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt new file mode 100644 index 000000000..cc49d43fd --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt @@ -0,0 +1,12 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt new file mode 100644 index 000000000..2c5924a7a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt @@ -0,0 +1,14 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt new file mode 100644 index 000000000..d5caa1bb9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt @@ -0,0 +1,31 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt new file mode 100644 index 000000000..2a476481a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt @@ -0,0 +1,12 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt new file mode 100644 index 000000000..663064a34 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt @@ -0,0 +1,11 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt new file mode 100644 index 000000000..3a48ba960 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt @@ -0,0 +1,12 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt new file mode 100644 index 000000000..db58b1346 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt @@ -0,0 +1,12 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt new file mode 100644 index 000000000..7996488be --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt @@ -0,0 +1,12 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.Predicate.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.Predicate.txt new file mode 100644 index 000000000..6367fe23c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.Predicate.txt @@ -0,0 +1,14 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt new file mode 100644 index 000000000..35c393b4e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt @@ -0,0 +1,11 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt new file mode 100644 index 000000000..ca17eb1dc --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt @@ -0,0 +1,15 @@ +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 %AutoForamt.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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt new file mode 100644 index 000000000..34657ba47 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt @@ -0,0 +1,46 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt new file mode 100644 index 000000000..dde990ab2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt @@ -0,0 +1,11 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowDuplicates.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowDuplicates.txt new file mode 100644 index 000000000..4d054b1f0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowDuplicates.txt @@ -0,0 +1,11 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt new file mode 100644 index 000000000..b324608f7 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt @@ -0,0 +1,8 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt new file mode 100644 index 000000000..748be0eec --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt @@ -0,0 +1,11 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt new file mode 100644 index 000000000..3fd465406 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt @@ -0,0 +1,12 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt new file mode 100644 index 000000000..460112ebe --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt @@ -0,0 +1,18 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt new file mode 100644 index 000000000..5cb7dda3b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt @@ -0,0 +1,11 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt new file mode 100644 index 000000000..f1f5c5f12 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt @@ -0,0 +1,13 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt new file mode 100644 index 000000000..7a3291470 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt @@ -0,0 +1,16 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt new file mode 100644 index 000000000..148eedb8b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt @@ -0,0 +1,10 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt new file mode 100644 index 000000000..e733a61e8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt @@ -0,0 +1,9 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt new file mode 100644 index 000000000..c486724c8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt @@ -0,0 +1,14 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt new file mode 100644 index 000000000..54036507d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt @@ -0,0 +1,13 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt new file mode 100644 index 000000000..2e0cc8104 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt @@ -0,0 +1,16 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt new file mode 100644 index 000000000..568cbf3b3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt @@ -0,0 +1,18 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyRemoveScript.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyRemoveScript.txt new file mode 100644 index 000000000..b2b6ab149 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyRemoveScript.txt @@ -0,0 +1,16 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AllowHostnameUnderscore.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AllowHostnameUnderscore.txt new file mode 100644 index 000000000..2c910cc7d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.AllowHostnameUnderscore.txt @@ -0,0 +1,16 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt new file mode 100644 index 000000000..d7317911f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt @@ -0,0 +1,12 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt new file mode 100644 index 000000000..c572c14ec --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt @@ -0,0 +1,29 @@ +Core.ColorKeywords +TYPE: hash +VERSION: 2.0.0 +--DEFAULT-- +array ( + 'maroon' => '#800000', + 'red' => '#FF0000', + 'orange' => '#FFA500', + 'yellow' => '#FFFF00', + 'olive' => '#808000', + 'purple' => '#800080', + 'fuchsia' => '#FF00FF', + 'white' => '#FFFFFF', + 'lime' => '#00FF00', + 'green' => '#008000', + 'navy' => '#000080', + 'blue' => '#0000FF', + 'aqua' => '#00FFFF', + 'teal' => '#008080', + 'black' => '#000000', + 'silver' => '#C0C0C0', + 'gray' => '#808080', +) +--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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt new file mode 100644 index 000000000..64b114fce --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt @@ -0,0 +1,14 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt new file mode 100644 index 000000000..36f16e07e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt @@ -0,0 +1,17 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt new file mode 100644 index 000000000..1cd4c2c96 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt @@ -0,0 +1,14 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt new file mode 100644 index 000000000..ce243c35d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt @@ -0,0 +1,9 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt new file mode 100644 index 000000000..8bfb47c3a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt @@ -0,0 +1,15 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt new file mode 100644 index 000000000..a3881be75 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt @@ -0,0 +1,12 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt new file mode 100644 index 000000000..a7a5b249b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt @@ -0,0 +1,7 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt new file mode 100644 index 000000000..abb499948 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt @@ -0,0 +1,13 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt new file mode 100644 index 000000000..915391edb --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt @@ -0,0 +1,19 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.Language.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.Language.txt new file mode 100644 index 000000000..233fca14f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.Language.txt @@ -0,0 +1,10 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt new file mode 100644 index 000000000..392b43649 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt @@ -0,0 +1,36 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt new file mode 100644 index 000000000..8983e2cca --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt @@ -0,0 +1,34 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt new file mode 100644 index 000000000..eb841a759 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt @@ -0,0 +1,16 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt new file mode 100644 index 000000000..d77f5360d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt @@ -0,0 +1,11 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt new file mode 100644 index 000000000..4070c2a0d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt @@ -0,0 +1,12 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt new file mode 100644 index 000000000..3397d9f71 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt @@ -0,0 +1,11 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt new file mode 100644 index 000000000..a4cd966df --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt @@ -0,0 +1,12 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt new file mode 100644 index 000000000..3db50ef20 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt @@ -0,0 +1,11 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt new file mode 100644 index 000000000..16829bcda --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt @@ -0,0 +1,14 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt new file mode 100644 index 000000000..7f95f54d1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt @@ -0,0 +1,29 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt new file mode 100644 index 000000000..6c231b2d7 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt @@ -0,0 +1,16 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt new file mode 100644 index 000000000..078d08741 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt @@ -0,0 +1,74 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt new file mode 100644 index 000000000..321eaa2d8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt @@ -0,0 +1,16 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt new file mode 100644 index 000000000..0b2c106da --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt @@ -0,0 +1,25 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt new file mode 100644 index 000000000..fcf093f17 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt @@ -0,0 +1,19 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt new file mode 100644 index 000000000..140e21423 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt @@ -0,0 +1,10 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt new file mode 100644 index 000000000..f22e977d4 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt @@ -0,0 +1,15 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt new file mode 100644 index 000000000..1d3fa7907 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt @@ -0,0 +1,23 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt new file mode 100644 index 000000000..5a59a55c0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt @@ -0,0 +1,20 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt new file mode 100644 index 000000000..151fb7b82 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt @@ -0,0 +1,11 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt new file mode 100644 index 000000000..45ae469ec --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt @@ -0,0 +1,18 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt new file mode 100644 index 000000000..524618879 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt @@ -0,0 +1,23 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt new file mode 100644 index 000000000..6ed70b599 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt @@ -0,0 +1,9 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt new file mode 100644 index 000000000..103db754a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt @@ -0,0 +1,33 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt new file mode 100644 index 000000000..229ae0267 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt @@ -0,0 +1,16 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt new file mode 100644 index 000000000..9dab497f2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt @@ -0,0 +1,11 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt new file mode 100644 index 000000000..7878dc0bf --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt @@ -0,0 +1,11 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt new file mode 100644 index 000000000..57358f9ba --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt @@ -0,0 +1,21 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt new file mode 100644 index 000000000..93a53e14f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt @@ -0,0 +1,20 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt new file mode 100644 index 000000000..e424c386e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt @@ -0,0 +1,14 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt new file mode 100644 index 000000000..700b30924 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt @@ -0,0 +1,7 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt new file mode 100644 index 000000000..62e8e160c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt @@ -0,0 +1,12 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt new file mode 100644 index 000000000..dfb720496 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt @@ -0,0 +1,12 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt new file mode 100644 index 000000000..cdda09a4c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt @@ -0,0 +1,13 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt new file mode 100644 index 000000000..5eb6ec2b5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt @@ -0,0 +1,13 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt new file mode 100644 index 000000000..ceb342e22 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt @@ -0,0 +1,13 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt new file mode 100644 index 000000000..5ebc7a19d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt @@ -0,0 +1,10 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt new file mode 100644 index 000000000..a8b1de56b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt @@ -0,0 +1,9 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt new file mode 100644 index 000000000..587a16778 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt @@ -0,0 +1,8 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt new file mode 100644 index 000000000..dd514c0de --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt @@ -0,0 +1,10 @@ +--# 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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt new file mode 100644 index 000000000..cb5a0b0e5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt @@ -0,0 +1,9 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt new file mode 100644 index 000000000..b4c271b7f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt @@ -0,0 +1,8 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt new file mode 100644 index 000000000..4186ccd0d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt @@ -0,0 +1,24 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt new file mode 100644 index 000000000..996762bd1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt @@ -0,0 +1,8 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt new file mode 100644 index 000000000..1db9237e9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt @@ -0,0 +1,9 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt new file mode 100644 index 000000000..2a47e384f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt @@ -0,0 +1,11 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt new file mode 100644 index 000000000..08921fde7 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt @@ -0,0 +1,10 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt new file mode 100644 index 000000000..d6f0d9f29 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt @@ -0,0 +1,15 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt new file mode 100644 index 000000000..93398e859 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt @@ -0,0 +1,11 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt new file mode 100644 index 000000000..79f8ad82c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt @@ -0,0 +1,13 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt new file mode 100644 index 000000000..232b02362 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt @@ -0,0 +1,14 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt new file mode 100644 index 000000000..06bab00a0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt @@ -0,0 +1,25 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt new file mode 100644 index 000000000..071bc0295 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt @@ -0,0 +1,7 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt new file mode 100644 index 000000000..eb97307e2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt @@ -0,0 +1,18 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Base.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Base.txt new file mode 100644 index 000000000..876f0680c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Base.txt @@ -0,0 +1,17 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt new file mode 100644 index 000000000..834bc08c0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt @@ -0,0 +1,15 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt new file mode 100644 index 000000000..f05312ba8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt @@ -0,0 +1,11 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt new file mode 100644 index 000000000..80cfea93f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt @@ -0,0 +1,11 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt new file mode 100644 index 000000000..71ce025a2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt @@ -0,0 +1,14 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt new file mode 100644 index 000000000..13c122c8c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt @@ -0,0 +1,11 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt new file mode 100644 index 000000000..abcc1efd6 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt @@ -0,0 +1,13 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt new file mode 100644 index 000000000..f891de499 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt @@ -0,0 +1,15 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Host.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Host.txt new file mode 100644 index 000000000..ee83b121d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Host.txt @@ -0,0 +1,19 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt new file mode 100644 index 000000000..0b6df7625 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt @@ -0,0 +1,9 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt new file mode 100644 index 000000000..4214900a5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt @@ -0,0 +1,13 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt new file mode 100644 index 000000000..58c81dcc4 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt @@ -0,0 +1,83 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt new file mode 100644 index 000000000..6fce0fdc3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt @@ -0,0 +1,17 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt new file mode 100644 index 000000000..1e17c1d46 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt @@ -0,0 +1,30 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt new file mode 100644 index 000000000..23331a4e7 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt @@ -0,0 +1,9 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt new file mode 100644 index 000000000..79084832b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt @@ -0,0 +1,22 @@ +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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/info.ini b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/info.ini new file mode 100644 index 000000000..5de4505e1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/info.ini @@ -0,0 +1,3 @@ +name = "HTML Purifier" + +; vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ContentSets.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ContentSets.php new file mode 100644 index 000000000..543e3f8f1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ContentSets.php @@ -0,0 +1,170 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Context.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Context.php new file mode 100644 index 000000000..00e509c85 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Context.php @@ -0,0 +1,95 @@ +<?php + +/** + * Registry object that contains information about the current context. + * @warning Is a bit buggy when variables are set to null: it thinks + * they don't exist! So use false instead, please. + * @note Since the variables Context deals with may not be objects, + * references are very important here! Do not remove! + */ +class HTMLPurifier_Context +{ + + /** + * Private array that stores the references. + * @type array + */ + private $_storage = array(); + + /** + * Registers a variable into the context. + * @param string $name String name + * @param mixed $ref Reference to variable to be registered + */ + public function register($name, &$ref) + { + if (array_key_exists($name, $this->_storage)) { + trigger_error( + "Name $name produces collision, cannot re-register", + E_USER_ERROR + ); + return; + } + $this->_storage[$name] =& $ref; + } + + /** + * Retrieves a variable reference from the context. + * @param string $name String name + * @param bool $ignore_error Boolean whether or not to ignore error + * @return mixed + */ + public function &get($name, $ignore_error = false) + { + if (!array_key_exists($name, $this->_storage)) { + if (!$ignore_error) { + trigger_error( + "Attempted to retrieve non-existent variable $name", + E_USER_ERROR + ); + } + $var = null; // so we can return by reference + return $var; + } + return $this->_storage[$name]; + } + + /** + * Destroys a variable in the context. + * @param string $name String name + */ + public function destroy($name) + { + if (!array_key_exists($name, $this->_storage)) { + trigger_error( + "Attempted to destroy non-existent variable $name", + E_USER_ERROR + ); + return; + } + unset($this->_storage[$name]); + } + + /** + * Checks whether or not the variable exists. + * @param string $name String name + * @return bool + */ + public function exists($name) + { + return array_key_exists($name, $this->_storage); + } + + /** + * Loads a series of variables from an associative array + * @param array $context_array Assoc array of variables to load + */ + public function loadArray($context_array) + { + foreach ($context_array as $key => $discard) { + $this->register($key, $context_array[$key]); + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Definition.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Definition.php new file mode 100644 index 000000000..bc6d43364 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Definition.php @@ -0,0 +1,55 @@ +<?php + +/** + * Super-class for definition datatype objects, implements serialization + * functions for the class. + */ +abstract class HTMLPurifier_Definition +{ + + /** + * Has setup() been called yet? + * @type bool + */ + public $setup = false; + + /** + * If true, write out the final definition object to the cache after + * setup. This will be true only if all invocations to get a raw + * definition object are also optimized. This does not cause file + * system thrashing because on subsequent calls the cached object + * is used and any writes to the raw definition object are short + * circuited. See enduser-customize.html for the high-level + * picture. + * @type bool + */ + public $optimized = null; + + /** + * What type of definition is it? + * @type string + */ + public $type; + + /** + * Sets up the definition object into the final form, something + * not done by the constructor + * @param HTMLPurifier_Config $config + */ + abstract protected function doSetup($config); + + /** + * Setup function that aborts if already setup + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + if ($this->setup) { + return; + } + $this->setup = true; + $this->doSetup($config); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache.php new file mode 100644 index 000000000..9aa8ff354 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache.php @@ -0,0 +1,129 @@ +<?php + +/** + * Abstract class representing Definition cache managers that implements + * useful common methods and is a factory. + * @todo Create a separate maintenance file advanced users can use to + * cache their custom HTMLDefinition, which can be loaded + * via a configuration directive + * @todo Implement memcached + */ +abstract class HTMLPurifier_DefinitionCache +{ + /** + * @type string + */ + public $type; + + /** + * @param string $type Type of definition objects this instance of the + * cache will handle. + */ + public function __construct($type) + { + $this->type = $type; + } + + /** + * Generates a unique identifier for a particular configuration + * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config + * @return string + */ + public function generateKey($config) + { + return $config->version . ',' . // possibly replace with function calls + $config->getBatchSerial($this->type) . ',' . + $config->get($this->type . '.DefinitionRev'); + } + + /** + * Tests whether or not a key is old with respect to the configuration's + * version and revision number. + * @param string $key Key to test + * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config to test against + * @return bool + */ + public function isOld($key, $config) + { + if (substr_count($key, ',') < 2) { + return true; + } + list($version, $hash, $revision) = explode(',', $key, 3); + $compare = version_compare($version, $config->version); + // version mismatch, is always old + if ($compare != 0) { + return true; + } + // versions match, ids match, check revision number + if ($hash == $config->getBatchSerial($this->type) && + $revision < $config->get($this->type . '.DefinitionRev')) { + return true; + } + return false; + } + + /** + * Checks if a definition's type jives with the cache's type + * @note Throws an error on failure + * @param HTMLPurifier_Definition $def Definition object to check + * @return bool true if good, false if not + */ + public function checkDefType($def) + { + if ($def->type !== $this->type) { + trigger_error("Cannot use definition of type {$def->type} in cache for {$this->type}"); + return false; + } + return true; + } + + /** + * Adds a definition object to the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function add($def, $config); + + /** + * Unconditionally saves a definition object to the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function set($def, $config); + + /** + * Replace an object in the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function replace($def, $config); + + /** + * Retrieves a definition object from the cache + * @param HTMLPurifier_Config $config + */ + abstract public function get($config); + + /** + * Removes a definition object to the cache + * @param HTMLPurifier_Config $config + */ + abstract public function remove($config); + + /** + * Clears all objects from cache + * @param HTMLPurifier_Config $config + */ + abstract public function flush($config); + + /** + * Clears all expired (older version or revision) objects from cache + * @note Be careful implementing this method as flush. Flush must + * not interfere with other Definition types, and cleanup() + * should not be repeatedly called by userland code. + * @param HTMLPurifier_Config $config + */ + abstract public function cleanup($config); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator.php new file mode 100644 index 000000000..b57a51b6c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator.php @@ -0,0 +1,112 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php new file mode 100644 index 000000000..4991777ce --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php @@ -0,0 +1,78 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Memory.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Memory.php new file mode 100644 index 000000000..d529dce48 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Memory.php @@ -0,0 +1,85 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Template.php.in b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Template.php.in new file mode 100644 index 000000000..b1fec8d36 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Decorator/Template.php.in @@ -0,0 +1,82 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Null.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Null.php new file mode 100644 index 000000000..d9a75ce22 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Null.php @@ -0,0 +1,76 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer.php new file mode 100644 index 000000000..952e48d47 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer.php @@ -0,0 +1,306 @@ +<?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) { + // TODO: This races + if (is_dir($directory)) return true; + return mkdir($directory); + } + 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)) { + 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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer/README b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer/README new file mode 100755 index 000000000..2e35c1c3d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer/README @@ -0,0 +1,3 @@ +This is a dummy file to prevent Git from ignoring this empty directory. + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCacheFactory.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCacheFactory.php new file mode 100644 index 000000000..fd1cc9be4 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCacheFactory.php @@ -0,0 +1,106 @@ +<?php + +/** + * Responsible for creating definition caches. + */ +class HTMLPurifier_DefinitionCacheFactory +{ + /** + * @type array + */ + protected $caches = array('Serializer' => array()); + + /** + * @type array + */ + protected $implementations = array(); + + /** + * @type HTMLPurifier_DefinitionCache_Decorator[] + */ + protected $decorators = array(); + + /** + * Initialize default decorators + */ + public function setup() + { + $this->addDecorator('Cleanup'); + } + + /** + * Retrieves an instance of global definition cache factory. + * @param HTMLPurifier_DefinitionCacheFactory $prototype + * @return HTMLPurifier_DefinitionCacheFactory + */ + public static function instance($prototype = null) + { + static $instance; + if ($prototype !== null) { + $instance = $prototype; + } elseif ($instance === null || $prototype === true) { + $instance = new HTMLPurifier_DefinitionCacheFactory(); + $instance->setup(); + } + return $instance; + } + + /** + * Registers a new definition cache object + * @param string $short Short name of cache object, for reference + * @param string $long Full class name of cache object, for construction + */ + public function register($short, $long) + { + $this->implementations[$short] = $long; + } + + /** + * Factory method that creates a cache object based on configuration + * @param string $type Name of definitions handled by cache + * @param HTMLPurifier_Config $config Config instance + * @return mixed + */ + public function create($type, $config) + { + $method = $config->get('Cache.DefinitionImpl'); + if ($method === null) { + return new HTMLPurifier_DefinitionCache_Null($type); + } + if (!empty($this->caches[$method][$type])) { + return $this->caches[$method][$type]; + } + if (isset($this->implementations[$method]) && + class_exists($class = $this->implementations[$method], false)) { + $cache = new $class($type); + } else { + if ($method != 'Serializer') { + trigger_error("Unrecognized DefinitionCache $method, using Serializer instead", E_USER_WARNING); + } + $cache = new HTMLPurifier_DefinitionCache_Serializer($type); + } + foreach ($this->decorators as $decorator) { + $new_cache = $decorator->decorate($cache); + // prevent infinite recursion in PHP 4 + unset($cache); + $cache = $new_cache; + } + $this->caches[$method][$type] = $cache; + return $this->caches[$method][$type]; + } + + /** + * Registers a decorator to add to all new cache objects + * @param HTMLPurifier_DefinitionCache_Decorator|string $decorator An instance or the name of a decorator + */ + public function addDecorator($decorator) + { + if (is_string($decorator)) { + $class = "HTMLPurifier_DefinitionCache_Decorator_$decorator"; + $decorator = new $class; + } + $this->decorators[$decorator->name] = $decorator; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Doctype.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Doctype.php new file mode 100644 index 000000000..4acd06e5b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Doctype.php @@ -0,0 +1,73 @@ +<?php + +/** + * Represents a document type, contains information on which modules + * need to be loaded. + * @note This class is inspected by Printer_HTMLDefinition->renderDoctype. + * If structure changes, please update that function. + */ +class HTMLPurifier_Doctype +{ + /** + * Full name of doctype + * @type string + */ + public $name; + + /** + * List of standard modules (string identifiers or literal objects) + * that this doctype uses + * @type array + */ + public $modules = array(); + + /** + * List of modules to use for tidying up code + * @type array + */ + public $tidyModules = array(); + + /** + * Is the language derived from XML (i.e. XHTML)? + * @type bool + */ + public $xml = true; + + /** + * List of aliases for this doctype + * @type array + */ + public $aliases = array(); + + /** + * Public DTD identifier + * @type string + */ + public $dtdPublic; + + /** + * System DTD identifier + * @type string + */ + public $dtdSystem; + + public function __construct( + $name = null, + $xml = true, + $modules = array(), + $tidyModules = array(), + $aliases = array(), + $dtd_public = null, + $dtd_system = null + ) { + $this->name = $name; + $this->xml = $xml; + $this->modules = $modules; + $this->tidyModules = $tidyModules; + $this->aliases = $aliases; + $this->dtdPublic = $dtd_public; + $this->dtdSystem = $dtd_system; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DoctypeRegistry.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DoctypeRegistry.php new file mode 100644 index 000000000..acc1d64a6 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/DoctypeRegistry.php @@ -0,0 +1,142 @@ +<?php + +class HTMLPurifier_DoctypeRegistry +{ + + /** + * Hash of doctype names to doctype objects. + * @type array + */ + protected $doctypes; + + /** + * Lookup table of aliases to real doctype names. + * @type array + */ + protected $aliases; + + /** + * Registers a doctype to the registry + * @note Accepts a fully-formed doctype object, or the + * parameters for constructing a doctype object + * @param string $doctype Name of doctype or literal doctype object + * @param bool $xml + * @param array $modules Modules doctype will load + * @param array $tidy_modules Modules doctype will load for certain modes + * @param array $aliases Alias names for doctype + * @param string $dtd_public + * @param string $dtd_system + * @return HTMLPurifier_Doctype Editable registered doctype + */ + public function register( + $doctype, + $xml = true, + $modules = array(), + $tidy_modules = array(), + $aliases = array(), + $dtd_public = null, + $dtd_system = null + ) { + if (!is_array($modules)) { + $modules = array($modules); + } + if (!is_array($tidy_modules)) { + $tidy_modules = array($tidy_modules); + } + if (!is_array($aliases)) { + $aliases = array($aliases); + } + if (!is_object($doctype)) { + $doctype = new HTMLPurifier_Doctype( + $doctype, + $xml, + $modules, + $tidy_modules, + $aliases, + $dtd_public, + $dtd_system + ); + } + $this->doctypes[$doctype->name] = $doctype; + $name = $doctype->name; + // hookup aliases + foreach ($doctype->aliases as $alias) { + if (isset($this->doctypes[$alias])) { + continue; + } + $this->aliases[$alias] = $name; + } + // remove old aliases + if (isset($this->aliases[$name])) { + unset($this->aliases[$name]); + } + return $doctype; + } + + /** + * Retrieves reference to a doctype of a certain name + * @note This function resolves aliases + * @note When possible, use the more fully-featured make() + * @param string $doctype Name of doctype + * @return HTMLPurifier_Doctype Editable doctype object + */ + public function get($doctype) + { + if (isset($this->aliases[$doctype])) { + $doctype = $this->aliases[$doctype]; + } + if (!isset($this->doctypes[$doctype])) { + trigger_error('Doctype ' . htmlspecialchars($doctype) . ' does not exist', E_USER_ERROR); + $anon = new HTMLPurifier_Doctype($doctype); + return $anon; + } + return $this->doctypes[$doctype]; + } + + /** + * Creates a doctype based on a configuration object, + * will perform initialization on the doctype + * @note Use this function to get a copy of doctype that config + * can hold on to (this is necessary in order to tell + * Generator whether or not the current document is XML + * based or not). + * @param HTMLPurifier_Config $config + * @return HTMLPurifier_Doctype + */ + public function make($config) + { + return clone $this->get($this->getDoctypeFromConfig($config)); + } + + /** + * Retrieves the doctype from the configuration object + * @param HTMLPurifier_Config $config + * @return string + */ + public function getDoctypeFromConfig($config) + { + // recommended test + $doctype = $config->get('HTML.Doctype'); + if (!empty($doctype)) { + return $doctype; + } + $doctype = $config->get('HTML.CustomDoctype'); + if (!empty($doctype)) { + return $doctype; + } + // backwards-compatibility + if ($config->get('HTML.XHTML')) { + $doctype = 'XHTML 1.0'; + } else { + $doctype = 'HTML 4.01'; + } + if ($config->get('HTML.Strict')) { + $doctype .= ' Strict'; + } else { + $doctype .= ' Transitional'; + } + return $doctype; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ElementDef.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ElementDef.php new file mode 100644 index 000000000..d5311cedc --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ElementDef.php @@ -0,0 +1,216 @@ +<?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", $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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Encoder.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Encoder.php new file mode 100644 index 000000000..b94f17542 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Encoder.php @@ -0,0 +1,617 @@ +<?php + +/** + * A UTF-8 specific character encoder that handles cleaning and transforming. + * @note All functions in this class should be static. + */ +class HTMLPurifier_Encoder +{ + + /** + * Constructor throws fatal error if you attempt to instantiate class + */ + private function __construct() + { + trigger_error('Cannot instantiate encoder, call methods statically', E_USER_ERROR); + } + + /** + * Error-handler that mutes errors, alternative to shut-up operator. + */ + public static function muteErrorHandler() + { + } + + /** + * iconv wrapper which mutes errors, but doesn't work around bugs. + * @param string $in Input encoding + * @param string $out Output encoding + * @param string $text The text to convert + * @return string + */ + public static function unsafeIconv($in, $out, $text) + { + set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler')); + $r = iconv($in, $out, $text); + restore_error_handler(); + return $r; + } + + /** + * iconv wrapper which mutes errors and works around bugs. + * @param string $in Input encoding + * @param string $out Output encoding + * @param string $text The text to convert + * @param int $max_chunk_size + * @return string + */ + public static function iconv($in, $out, $text, $max_chunk_size = 8000) + { + $code = self::testIconvTruncateBug(); + if ($code == self::ICONV_OK) { + return self::unsafeIconv($in, $out, $text); + } elseif ($code == self::ICONV_TRUNCATES) { + // we can only work around this if the input character set + // is utf-8 + if ($in == 'utf-8') { + if ($max_chunk_size < 4) { + trigger_error('max_chunk_size is too small', E_USER_WARNING); + return false; + } + // split into 8000 byte chunks, but be careful to handle + // multibyte boundaries properly + if (($c = strlen($text)) <= $max_chunk_size) { + return self::unsafeIconv($in, $out, $text); + } + $r = ''; + $i = 0; + while (true) { + if ($i + $max_chunk_size >= $c) { + $r .= self::unsafeIconv($in, $out, substr($text, $i)); + break; + } + // wibble the boundary + if (0x80 != (0xC0 & ord($text[$i + $max_chunk_size]))) { + $chunk_size = $max_chunk_size; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 1]))) { + $chunk_size = $max_chunk_size - 1; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 2]))) { + $chunk_size = $max_chunk_size - 2; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 3]))) { + $chunk_size = $max_chunk_size - 3; + } else { + return false; // rather confusing UTF-8... + } + $chunk = substr($text, $i, $chunk_size); // substr doesn't mind overlong lengths + $r .= self::unsafeIconv($in, $out, $chunk); + $i += $chunk_size; + } + return $r; + } else { + return false; + } + } else { + return false; + } + } + + /** + * Cleans a UTF-8 string for well-formedness and SGML validity + * + * It will parse according to UTF-8 and return a valid UTF8 string, with + * non-SGML codepoints excluded. + * + * Specifically, it will permit: + * \x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF} + * Source: https://www.w3.org/TR/REC-xml/#NT-Char + * Arguably this function should be modernized to the HTML5 set + * of allowed characters: + * https://www.w3.org/TR/html5/syntax.html#preprocessing-the-input-stream + * which simultaneously expand and restrict the set of allowed characters. + * + * @param string $str The string to clean + * @param bool $force_php + * @return string + * + * @note Just for reference, the non-SGML code points are 0 to 31 and + * 127 to 159, inclusive. However, we allow code points 9, 10 + * and 13, which are the tab, line feed and carriage return + * respectively. 128 and above the code points map to multibyte + * UTF-8 representations. + * + * @note Fallback code adapted from utf8ToUnicode by Henri Sivonen and + * hsivonen@iki.fi at <http://iki.fi/hsivonen/php-utf8/> under the + * LGPL license. Notes on what changed are inside, but in general, + * the original code transformed UTF-8 text into an array of integer + * Unicode codepoints. Understandably, transforming that back to + * a string would be somewhat expensive, so the function was modded to + * directly operate on the string. However, this discourages code + * reuse, and the logic enumerated here would be useful for any + * function that needs to be able to understand UTF-8 characters. + * As of right now, only smart lossless character encoding converters + * would need that, and I'm probably not going to implement them. + */ + public static function cleanUTF8($str, $force_php = false) + { + // UTF-8 validity is checked since PHP 4.3.5 + // This is an optimization: if the string is already valid UTF-8, no + // need to do PHP stuff. 99% of the time, this will be the case. + if (preg_match( + '/^[\x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]*$/Du', + $str + )) { + return $str; + } + + $mState = 0; // cached expected number of octets after the current octet + // until the beginning of the next UTF8 character sequence + $mUcs4 = 0; // cached Unicode character + $mBytes = 1; // cached expected number of octets in the current sequence + + // original code involved an $out that was an array of Unicode + // codepoints. Instead of having to convert back into UTF-8, we've + // decided to directly append valid UTF-8 characters onto a string + // $out once they're done. $char accumulates raw bytes, while $mUcs4 + // turns into the Unicode code point, so there's some redundancy. + + $out = ''; + $char = ''; + + $len = strlen($str); + for ($i = 0; $i < $len; $i++) { + $in = ord($str{$i}); + $char .= $str[$i]; // append byte to char + if (0 == $mState) { + // When mState is zero we expect either a US-ASCII character + // or a multi-octet sequence. + if (0 == (0x80 & ($in))) { + // US-ASCII, pass straight through. + if (($in <= 31 || $in == 127) && + !($in == 9 || $in == 13 || $in == 10) // save \r\t\n + ) { + // control characters, remove + } else { + $out .= $char; + } + // reset + $char = ''; + $mBytes = 1; + } elseif (0xC0 == (0xE0 & ($in))) { + // First octet of 2 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x1F) << 6; + $mState = 1; + $mBytes = 2; + } elseif (0xE0 == (0xF0 & ($in))) { + // First octet of 3 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x0F) << 12; + $mState = 2; + $mBytes = 3; + } elseif (0xF0 == (0xF8 & ($in))) { + // First octet of 4 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x07) << 18; + $mState = 3; + $mBytes = 4; + } elseif (0xF8 == (0xFC & ($in))) { + // First octet of 5 octet sequence. + // + // This is illegal because the encoded codepoint must be + // either: + // (a) not the shortest form or + // (b) outside the Unicode range of 0-0x10FFFF. + // Rather than trying to resynchronize, we will carry on + // until the end of the sequence and let the later error + // handling code catch it. + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x03) << 24; + $mState = 4; + $mBytes = 5; + } elseif (0xFC == (0xFE & ($in))) { + // First octet of 6 octet sequence, see comments for 5 + // octet sequence. + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 1) << 30; + $mState = 5; + $mBytes = 6; + } else { + // Current octet is neither in the US-ASCII range nor a + // legal first octet of a multi-octet sequence. + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + $char = ''; + } + } else { + // When mState is non-zero, we expect a continuation of the + // multi-octet sequence + if (0x80 == (0xC0 & ($in))) { + // Legal continuation. + $shift = ($mState - 1) * 6; + $tmp = $in; + $tmp = ($tmp & 0x0000003F) << $shift; + $mUcs4 |= $tmp; + + if (0 == --$mState) { + // End of the multi-octet sequence. mUcs4 now contains + // the final Unicode codepoint to be output + + // Check for illegal sequences and codepoints. + + // From Unicode 3.1, non-shortest form is illegal + if (((2 == $mBytes) && ($mUcs4 < 0x0080)) || + ((3 == $mBytes) && ($mUcs4 < 0x0800)) || + ((4 == $mBytes) && ($mUcs4 < 0x10000)) || + (4 < $mBytes) || + // From Unicode 3.2, surrogate characters = illegal + (($mUcs4 & 0xFFFFF800) == 0xD800) || + // Codepoints outside the Unicode range are illegal + ($mUcs4 > 0x10FFFF) + ) { + + } elseif (0xFEFF != $mUcs4 && // omit BOM + // check for valid Char unicode codepoints + ( + 0x9 == $mUcs4 || + 0xA == $mUcs4 || + 0xD == $mUcs4 || + (0x20 <= $mUcs4 && 0x7E >= $mUcs4) || + // 7F-9F is not strictly prohibited by XML, + // but it is non-SGML, and thus we don't allow it + (0xA0 <= $mUcs4 && 0xD7FF >= $mUcs4) || + (0xE000 <= $mUcs4 && 0xFFFD >= $mUcs4) || + (0x10000 <= $mUcs4 && 0x10FFFF >= $mUcs4) + ) + ) { + $out .= $char; + } + // initialize UTF8 cache (reset) + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + $char = ''; + } + } else { + // ((0xC0 & (*in) != 0x80) && (mState != 0)) + // Incomplete multi-octet sequence. + // used to result in complete fail, but we'll reset + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + $char =''; + } + } + } + return $out; + } + + /** + * Translates a Unicode codepoint into its corresponding UTF-8 character. + * @note Based on Feyd's function at + * <http://forums.devnetwork.net/viewtopic.php?p=191404#191404>, + * which is in public domain. + * @note While we're going to do code point parsing anyway, a good + * optimization would be to refuse to translate code points that + * are non-SGML characters. However, this could lead to duplication. + * @note This is very similar to the unichr function in + * maintenance/generate-entity-file.php (although this is superior, + * due to its sanity checks). + */ + + // +----------+----------+----------+----------+ + // | 33222222 | 22221111 | 111111 | | + // | 10987654 | 32109876 | 54321098 | 76543210 | bit + // +----------+----------+----------+----------+ + // | | | | 0xxxxxxx | 1 byte 0x00000000..0x0000007F + // | | | 110yyyyy | 10xxxxxx | 2 byte 0x00000080..0x000007FF + // | | 1110zzzz | 10yyyyyy | 10xxxxxx | 3 byte 0x00000800..0x0000FFFF + // | 11110www | 10wwzzzz | 10yyyyyy | 10xxxxxx | 4 byte 0x00010000..0x0010FFFF + // +----------+----------+----------+----------+ + // | 00000000 | 00011111 | 11111111 | 11111111 | Theoretical upper limit of legal scalars: 2097151 (0x001FFFFF) + // | 00000000 | 00010000 | 11111111 | 11111111 | Defined upper limit of legal scalar codes + // +----------+----------+----------+----------+ + + public static function unichr($code) + { + if ($code > 1114111 or $code < 0 or + ($code >= 55296 and $code <= 57343) ) { + // bits are set outside the "valid" range as defined + // by UNICODE 4.1.0 + return ''; + } + + $x = $y = $z = $w = 0; + if ($code < 128) { + // regular ASCII character + $x = $code; + } else { + // set up bits for UTF-8 + $x = ($code & 63) | 128; + if ($code < 2048) { + $y = (($code & 2047) >> 6) | 192; + } else { + $y = (($code & 4032) >> 6) | 128; + if ($code < 65536) { + $z = (($code >> 12) & 15) | 224; + } else { + $z = (($code >> 12) & 63) | 128; + $w = (($code >> 18) & 7) | 240; + } + } + } + // set up the actual character + $ret = ''; + if ($w) { + $ret .= chr($w); + } + if ($z) { + $ret .= chr($z); + } + if ($y) { + $ret .= chr($y); + } + $ret .= chr($x); + + return $ret; + } + + /** + * @return bool + */ + public static function iconvAvailable() + { + static $iconv = null; + if ($iconv === null) { + $iconv = function_exists('iconv') && self::testIconvTruncateBug() != self::ICONV_UNUSABLE; + } + return $iconv; + } + + /** + * Convert a string to UTF-8 based on configuration. + * @param string $str The string to convert + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public static function convertToUTF8($str, $config, $context) + { + $encoding = $config->get('Core.Encoding'); + if ($encoding === 'utf-8') { + return $str; + } + static $iconv = null; + if ($iconv === null) { + $iconv = self::iconvAvailable(); + } + if ($iconv && !$config->get('Test.ForceNoIconv')) { + // unaffected by bugs, since UTF-8 support all characters + $str = self::unsafeIconv($encoding, 'utf-8//IGNORE', $str); + if ($str === false) { + // $encoding is not a valid encoding + trigger_error('Invalid encoding ' . $encoding, E_USER_ERROR); + return ''; + } + // If the string is bjorked by Shift_JIS or a similar encoding + // that doesn't support all of ASCII, convert the naughty + // characters to their true byte-wise ASCII/UTF-8 equivalents. + $str = strtr($str, self::testEncodingSupportsASCII($encoding)); + return $str; + } elseif ($encoding === 'iso-8859-1') { + $str = utf8_encode($str); + return $str; + } + $bug = HTMLPurifier_Encoder::testIconvTruncateBug(); + if ($bug == self::ICONV_OK) { + trigger_error('Encoding not supported, please install iconv', E_USER_ERROR); + } else { + trigger_error( + 'You have a buggy version of iconv, see https://bugs.php.net/bug.php?id=48147 ' . + 'and http://sourceware.org/bugzilla/show_bug.cgi?id=13541', + E_USER_ERROR + ); + } + } + + /** + * Converts a string from UTF-8 based on configuration. + * @param string $str The string to convert + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + * @note Currently, this is a lossy conversion, with unexpressable + * characters being omitted. + */ + public static function convertFromUTF8($str, $config, $context) + { + $encoding = $config->get('Core.Encoding'); + if ($escape = $config->get('Core.EscapeNonASCIICharacters')) { + $str = self::convertToASCIIDumbLossless($str); + } + if ($encoding === 'utf-8') { + return $str; + } + static $iconv = null; + if ($iconv === null) { + $iconv = self::iconvAvailable(); + } + if ($iconv && !$config->get('Test.ForceNoIconv')) { + // Undo our previous fix in convertToUTF8, otherwise iconv will barf + $ascii_fix = self::testEncodingSupportsASCII($encoding); + if (!$escape && !empty($ascii_fix)) { + $clear_fix = array(); + foreach ($ascii_fix as $utf8 => $native) { + $clear_fix[$utf8] = ''; + } + $str = strtr($str, $clear_fix); + } + $str = strtr($str, array_flip($ascii_fix)); + // Normal stuff + $str = self::iconv('utf-8', $encoding . '//IGNORE', $str); + return $str; + } elseif ($encoding === 'iso-8859-1') { + $str = utf8_decode($str); + return $str; + } + trigger_error('Encoding not supported', E_USER_ERROR); + // You might be tempted to assume that the ASCII representation + // might be OK, however, this is *not* universally true over all + // encodings. So we take the conservative route here, rather + // than forcibly turn on %Core.EscapeNonASCIICharacters + } + + /** + * Lossless (character-wise) conversion of HTML to ASCII + * @param string $str UTF-8 string to be converted to ASCII + * @return string ASCII encoded string with non-ASCII character entity-ized + * @warning Adapted from MediaWiki, claiming fair use: this is a common + * algorithm. If you disagree with this license fudgery, + * implement it yourself. + * @note Uses decimal numeric entities since they are best supported. + * @note This is a DUMB function: it has no concept of keeping + * character entities that the projected character encoding + * can allow. We could possibly implement a smart version + * but that would require it to also know which Unicode + * codepoints the charset supported (not an easy task). + * @note Sort of with cleanUTF8() but it assumes that $str is + * well-formed UTF-8 + */ + public static function convertToASCIIDumbLossless($str) + { + $bytesleft = 0; + $result = ''; + $working = 0; + $len = strlen($str); + for ($i = 0; $i < $len; $i++) { + $bytevalue = ord($str[$i]); + if ($bytevalue <= 0x7F) { //0xxx xxxx + $result .= chr($bytevalue); + $bytesleft = 0; + } elseif ($bytevalue <= 0xBF) { //10xx xxxx + $working = $working << 6; + $working += ($bytevalue & 0x3F); + $bytesleft--; + if ($bytesleft <= 0) { + $result .= "&#" . $working . ";"; + } + } elseif ($bytevalue <= 0xDF) { //110x xxxx + $working = $bytevalue & 0x1F; + $bytesleft = 1; + } elseif ($bytevalue <= 0xEF) { //1110 xxxx + $working = $bytevalue & 0x0F; + $bytesleft = 2; + } else { //1111 0xxx + $working = $bytevalue & 0x07; + $bytesleft = 3; + } + } + return $result; + } + + /** No bugs detected in iconv. */ + const ICONV_OK = 0; + + /** Iconv truncates output if converting from UTF-8 to another + * character set with //IGNORE, and a non-encodable character is found */ + const ICONV_TRUNCATES = 1; + + /** Iconv does not support //IGNORE, making it unusable for + * transcoding purposes */ + const ICONV_UNUSABLE = 2; + + /** + * glibc iconv has a known bug where it doesn't handle the magic + * //IGNORE stanza correctly. In particular, rather than ignore + * characters, it will return an EILSEQ after consuming some number + * of characters, and expect you to restart iconv as if it were + * an E2BIG. Old versions of PHP did not respect the errno, and + * returned the fragment, so as a result you would see iconv + * mysteriously truncating output. We can work around this by + * manually chopping our input into segments of about 8000 + * characters, as long as PHP ignores the error code. If PHP starts + * paying attention to the error code, iconv becomes unusable. + * + * @return int Error code indicating severity of bug. + */ + public static function testIconvTruncateBug() + { + static $code = null; + if ($code === null) { + // better not use iconv, otherwise infinite loop! + $r = self::unsafeIconv('utf-8', 'ascii//IGNORE', "\xCE\xB1" . str_repeat('a', 9000)); + if ($r === false) { + $code = self::ICONV_UNUSABLE; + } elseif (($c = strlen($r)) < 9000) { + $code = self::ICONV_TRUNCATES; + } elseif ($c > 9000) { + trigger_error( + 'Your copy of iconv is extremely buggy. Please notify HTML Purifier maintainers: ' . + 'include your iconv version as per phpversion()', + E_USER_ERROR + ); + } else { + $code = self::ICONV_OK; + } + } + return $code; + } + + /** + * This expensive function tests whether or not a given character + * encoding supports ASCII. 7/8-bit encodings like Shift_JIS will + * fail this test, and require special processing. Variable width + * encodings shouldn't ever fail. + * + * @param string $encoding Encoding name to test, as per iconv format + * @param bool $bypass Whether or not to bypass the precompiled arrays. + * @return Array of UTF-8 characters to their corresponding ASCII, + * which can be used to "undo" any overzealous iconv action. + */ + public static function testEncodingSupportsASCII($encoding, $bypass = false) + { + // All calls to iconv here are unsafe, proof by case analysis: + // If ICONV_OK, no difference. + // If ICONV_TRUNCATE, all calls involve one character inputs, + // so bug is not triggered. + // If ICONV_UNUSABLE, this call is irrelevant + static $encodings = array(); + if (!$bypass) { + if (isset($encodings[$encoding])) { + return $encodings[$encoding]; + } + $lenc = strtolower($encoding); + switch ($lenc) { + case 'shift_jis': + return array("\xC2\xA5" => '\\', "\xE2\x80\xBE" => '~'); + case 'johab': + return array("\xE2\x82\xA9" => '\\'); + } + if (strpos($lenc, 'iso-8859-') === 0) { + return array(); + } + } + $ret = array(); + if (self::unsafeIconv('UTF-8', $encoding, 'a') === false) { + return false; + } + for ($i = 0x20; $i <= 0x7E; $i++) { // all printable ASCII chars + $c = chr($i); // UTF-8 char + $r = self::unsafeIconv('UTF-8', "$encoding//IGNORE", $c); // initial conversion + if ($r === '' || + // This line is needed for iconv implementations that do not + // omit characters that do not exist in the target character set + ($r === $c && self::unsafeIconv($encoding, 'UTF-8//IGNORE', $r) !== $c) + ) { + // Reverse engineer: what's the UTF-8 equiv of this byte + // sequence? This assumes that there's no variable width + // encoding that doesn't support ASCII. + $ret[self::unsafeIconv($encoding, 'UTF-8//IGNORE', $c)] = $c; + } + } + $encodings[$encoding] = $ret; + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup.php new file mode 100644 index 000000000..f12ff13a3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup.php @@ -0,0 +1,48 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup/entities.ser b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup/entities.ser new file mode 100644 index 000000000..e8b08128b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityLookup/entities.ser @@ -0,0 +1 @@ +a:253:{s:4:"fnof";s:2:"ƒ";s:5:"Alpha";s:2:"Α";s:4:"Beta";s:2:"Β";s:5:"Gamma";s:2:"Γ";s:5:"Delta";s:2:"Δ";s:7:"Epsilon";s:2:"Ε";s:4:"Zeta";s:2:"Ζ";s:3:"Eta";s:2:"Η";s:5:"Theta";s:2:"Θ";s:4:"Iota";s:2:"Ι";s:5:"Kappa";s:2:"Κ";s:6:"Lambda";s:2:"Λ";s:2:"Mu";s:2:"Μ";s:2:"Nu";s:2:"Ν";s:2:"Xi";s:2:"Ξ";s:7:"Omicron";s:2:"Ο";s:2:"Pi";s:2:"Π";s:3:"Rho";s:2:"Ρ";s:5:"Sigma";s:2:"Σ";s:3:"Tau";s:2:"Τ";s:7:"Upsilon";s:2:"Υ";s:3:"Phi";s:2:"Φ";s:3:"Chi";s:2:"Χ";s:3:"Psi";s:2:"Ψ";s:5:"Omega";s:2:"Ω";s:5:"alpha";s:2:"α";s:4:"beta";s:2:"β";s:5:"gamma";s:2:"γ";s:5:"delta";s:2:"δ";s:7:"epsilon";s:2:"ε";s:4:"zeta";s:2:"ζ";s:3:"eta";s:2:"η";s:5:"theta";s:2:"θ";s:4:"iota";s:2:"ι";s:5:"kappa";s:2:"κ";s:6:"lambda";s:2:"λ";s:2:"mu";s:2:"μ";s:2:"nu";s:2:"ν";s:2:"xi";s:2:"ξ";s:7:"omicron";s:2:"ο";s:2:"pi";s:2:"π";s:3:"rho";s:2:"ρ";s:6:"sigmaf";s:2:"ς";s:5:"sigma";s:2:"σ";s:3:"tau";s:2:"τ";s:7:"upsilon";s:2:"υ";s:3:"phi";s:2:"φ";s:3:"chi";s:2:"χ";s:3:"psi";s:2:"ψ";s:5:"omega";s:2:"ω";s:8:"thetasym";s:2:"ϑ";s:5:"upsih";s:2:"ϒ";s:3:"piv";s:2:"ϖ";s:4:"bull";s:3:"•";s:6:"hellip";s:3:"…";s:5:"prime";s:3:"′";s:5:"Prime";s:3:"″";s:5:"oline";s:3:"‾";s:5:"frasl";s:3:"⁄";s:6:"weierp";s:3:"℘";s:5:"image";s:3:"ℑ";s:4:"real";s:3:"ℜ";s:5:"trade";s:3:"™";s:7:"alefsym";s:3:"ℵ";s:4:"larr";s:3:"←";s:4:"uarr";s:3:"↑";s:4:"rarr";s:3:"→";s:4:"darr";s:3:"↓";s:4:"harr";s:3:"↔";s:5:"crarr";s:3:"↵";s:4:"lArr";s:3:"⇐";s:4:"uArr";s:3:"⇑";s:4:"rArr";s:3:"⇒";s:4:"dArr";s:3:"⇓";s:4:"hArr";s:3:"⇔";s:6:"forall";s:3:"∀";s:4:"part";s:3:"∂";s:5:"exist";s:3:"∃";s:5:"empty";s:3:"∅";s:5:"nabla";s:3:"∇";s:4:"isin";s:3:"∈";s:5:"notin";s:3:"∉";s:2:"ni";s:3:"∋";s:4:"prod";s:3:"∏";s:3:"sum";s:3:"∑";s:5:"minus";s:3:"−";s:6:"lowast";s:3:"∗";s:5:"radic";s:3:"√";s:4:"prop";s:3:"∝";s:5:"infin";s:3:"∞";s:3:"ang";s:3:"∠";s:3:"and";s:3:"∧";s:2:"or";s:3:"∨";s:3:"cap";s:3:"∩";s:3:"cup";s:3:"∪";s:3:"int";s:3:"∫";s:6:"there4";s:3:"∴";s:3:"sim";s:3:"∼";s:4:"cong";s:3:"≅";s:5:"asymp";s:3:"≈";s:2:"ne";s:3:"≠";s:5:"equiv";s:3:"≡";s:2:"le";s:3:"≤";s:2:"ge";s:3:"≥";s:3:"sub";s:3:"⊂";s:3:"sup";s:3:"⊃";s:4:"nsub";s:3:"⊄";s:4:"sube";s:3:"⊆";s:4:"supe";s:3:"⊇";s:5:"oplus";s:3:"⊕";s:6:"otimes";s:3:"⊗";s:4:"perp";s:3:"⊥";s:4:"sdot";s:3:"⋅";s:5:"lceil";s:3:"⌈";s:5:"rceil";s:3:"⌉";s:6:"lfloor";s:3:"⌊";s:6:"rfloor";s:3:"⌋";s:4:"lang";s:3:"〈";s:4:"rang";s:3:"〉";s:3:"loz";s:3:"◊";s:6:"spades";s:3:"♠";s:5:"clubs";s:3:"♣";s:6:"hearts";s:3:"♥";s:5:"diams";s:3:"♦";s:4:"quot";s:1:""";s:3:"amp";s:1:"&";s:2:"lt";s:1:"<";s:2:"gt";s:1:">";s:4:"apos";s:1:"'";s:5:"OElig";s:2:"Œ";s:5:"oelig";s:2:"œ";s:6:"Scaron";s:2:"Š";s:6:"scaron";s:2:"š";s:4:"Yuml";s:2:"Ÿ";s:4:"circ";s:2:"ˆ";s:5:"tilde";s:2:"˜";s:4:"ensp";s:3:" ";s:4:"emsp";s:3:" ";s:6:"thinsp";s:3:" ";s:4:"zwnj";s:3:"";s:3:"zwj";s:3:"";s:3:"lrm";s:3:"";s:3:"rlm";s:3:"";s:5:"ndash";s:3:"–";s:5:"mdash";s:3:"—";s:5:"lsquo";s:3:"‘";s:5:"rsquo";s:3:"’";s:5:"sbquo";s:3:"‚";s:5:"ldquo";s:3:"“";s:5:"rdquo";s:3:"”";s:5:"bdquo";s:3:"„";s:6:"dagger";s:3:"†";s:6:"Dagger";s:3:"‡";s:6:"permil";s:3:"‰";s:6:"lsaquo";s:3:"‹";s:6:"rsaquo";s:3:"›";s:4:"euro";s:3:"€";s:4:"nbsp";s:2:" ";s:5:"iexcl";s:2:"¡";s:4:"cent";s:2:"¢";s:5:"pound";s:2:"£";s:6:"curren";s:2:"¤";s:3:"yen";s:2:"¥";s:6:"brvbar";s:2:"¦";s:4:"sect";s:2:"§";s:3:"uml";s:2:"¨";s:4:"copy";s:2:"©";s:4:"ordf";s:2:"ª";s:5:"laquo";s:2:"«";s:3:"not";s:2:"¬";s:3:"shy";s:2:"";s:3:"reg";s:2:"®";s:4:"macr";s:2:"¯";s:3:"deg";s:2:"°";s:6:"plusmn";s:2:"±";s:4:"sup2";s:2:"²";s:4:"sup3";s:2:"³";s:5:"acute";s:2:"´";s:5:"micro";s:2:"µ";s:4:"para";s:2:"¶";s:6:"middot";s:2:"·";s:5:"cedil";s:2:"¸";s:4:"sup1";s:2:"¹";s:4:"ordm";s:2:"º";s:5:"raquo";s:2:"»";s:6:"frac14";s:2:"¼";s:6:"frac12";s:2:"½";s:6:"frac34";s:2:"¾";s:6:"iquest";s:2:"¿";s:6:"Agrave";s:2:"À";s:6:"Aacute";s:2:"Á";s:5:"Acirc";s:2:"Â";s:6:"Atilde";s:2:"Ã";s:4:"Auml";s:2:"Ä";s:5:"Aring";s:2:"Å";s:5:"AElig";s:2:"Æ";s:6:"Ccedil";s:2:"Ç";s:6:"Egrave";s:2:"È";s:6:"Eacute";s:2:"É";s:5:"Ecirc";s:2:"Ê";s:4:"Euml";s:2:"Ë";s:6:"Igrave";s:2:"Ì";s:6:"Iacute";s:2:"Í";s:5:"Icirc";s:2:"Î";s:4:"Iuml";s:2:"Ï";s:3:"ETH";s:2:"Ð";s:6:"Ntilde";s:2:"Ñ";s:6:"Ograve";s:2:"Ò";s:6:"Oacute";s:2:"Ó";s:5:"Ocirc";s:2:"Ô";s:6:"Otilde";s:2:"Õ";s:4:"Ouml";s:2:"Ö";s:5:"times";s:2:"×";s:6:"Oslash";s:2:"Ø";s:6:"Ugrave";s:2:"Ù";s:6:"Uacute";s:2:"Ú";s:5:"Ucirc";s:2:"Û";s:4:"Uuml";s:2:"Ü";s:6:"Yacute";s:2:"Ý";s:5:"THORN";s:2:"Þ";s:5:"szlig";s:2:"ß";s:6:"agrave";s:2:"à";s:6:"aacute";s:2:"á";s:5:"acirc";s:2:"â";s:6:"atilde";s:2:"ã";s:4:"auml";s:2:"ä";s:5:"aring";s:2:"å";s:5:"aelig";s:2:"æ";s:6:"ccedil";s:2:"ç";s:6:"egrave";s:2:"è";s:6:"eacute";s:2:"é";s:5:"ecirc";s:2:"ê";s:4:"euml";s:2:"ë";s:6:"igrave";s:2:"ì";s:6:"iacute";s:2:"í";s:5:"icirc";s:2:"î";s:4:"iuml";s:2:"ï";s:3:"eth";s:2:"ð";s:6:"ntilde";s:2:"ñ";s:6:"ograve";s:2:"ò";s:6:"oacute";s:2:"ó";s:5:"ocirc";s:2:"ô";s:6:"otilde";s:2:"õ";s:4:"ouml";s:2:"ö";s:6:"divide";s:2:"÷";s:6:"oslash";s:2:"ø";s:6:"ugrave";s:2:"ù";s:6:"uacute";s:2:"ú";s:5:"ucirc";s:2:"û";s:4:"uuml";s:2:"ü";s:6:"yacute";s:2:"ý";s:5:"thorn";s:2:"þ";s:4:"yuml";s:2:"ÿ";}
\ No newline at end of file diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityParser.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityParser.php new file mode 100644 index 000000000..c372b5a6a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/EntityParser.php @@ -0,0 +1,285 @@ +<?php + +// if want to implement error collecting here, we'll need to use some sort +// of global data (probably trigger_error) because it's impossible to pass +// $config or $context to the callback functions. + +/** + * Handles referencing and derefencing character entities + */ +class HTMLPurifier_EntityParser +{ + + /** + * Reference to entity lookup table. + * @type HTMLPurifier_EntityLookup + */ + protected $_entity_lookup; + + /** + * Callback regex string for entities in text. + * @type string + */ + protected $_textEntitiesRegex; + + /** + * Callback regex string for entities in attributes. + * @type string + */ + protected $_attrEntitiesRegex; + + /** + * Tests if the beginning of a string is a semi-optional regex + */ + protected $_semiOptionalPrefixRegex; + + public function __construct() { + // From + // http://stackoverflow.com/questions/15532252/why-is-reg-being-rendered-as-without-the-bounding-semicolon + $semi_optional = "quot|QUOT|lt|LT|gt|GT|amp|AMP|AElig|Aacute|Acirc|Agrave|Aring|Atilde|Auml|COPY|Ccedil|ETH|Eacute|Ecirc|Egrave|Euml|Iacute|Icirc|Igrave|Iuml|Ntilde|Oacute|Ocirc|Ograve|Oslash|Otilde|Ouml|REG|THORN|Uacute|Ucirc|Ugrave|Uuml|Yacute|aacute|acirc|acute|aelig|agrave|aring|atilde|auml|brvbar|ccedil|cedil|cent|copy|curren|deg|divide|eacute|ecirc|egrave|eth|euml|frac12|frac14|frac34|iacute|icirc|iexcl|igrave|iquest|iuml|laquo|macr|micro|middot|nbsp|not|ntilde|oacute|ocirc|ograve|ordf|ordm|oslash|otilde|ouml|para|plusmn|pound|raquo|reg|sect|shy|sup1|sup2|sup3|szlig|thorn|times|uacute|ucirc|ugrave|uml|uuml|yacute|yen|yuml"; + + // NB: three empty captures to put the fourth match in the right + // place + $this->_semiOptionalPrefixRegex = "/&()()()($semi_optional)/"; + + $this->_textEntitiesRegex = + '/&(?:'. + // hex + '[#]x([a-fA-F0-9]+);?|'. + // dec + '[#]0*(\d+);?|'. + // string (mandatory semicolon) + // NB: order matters: match semicolon preferentially + '([A-Za-z_:][A-Za-z0-9.\-_:]*);|'. + // string (optional semicolon) + "($semi_optional)". + ')/'; + + $this->_attrEntitiesRegex = + '/&(?:'. + // hex + '[#]x([a-fA-F0-9]+);?|'. + // dec + '[#]0*(\d+);?|'. + // string (mandatory semicolon) + // NB: order matters: match semicolon preferentially + '([A-Za-z_:][A-Za-z0-9.\-_:]*);|'. + // string (optional semicolon) + // don't match if trailing is equals or alphanumeric (URL + // like) + "($semi_optional)(?![=;A-Za-z0-9])". + ')/'; + + } + + /** + * Substitute entities with the parsed equivalents. Use this on + * textual data in an HTML document (as opposed to attributes.) + * + * @param string $string String to have entities parsed. + * @return string Parsed string. + */ + public function substituteTextEntities($string) + { + return preg_replace_callback( + $this->_textEntitiesRegex, + array($this, 'entityCallback'), + $string + ); + } + + /** + * Substitute entities with the parsed equivalents. Use this on + * attribute contents in documents. + * + * @param string $string String to have entities parsed. + * @return string Parsed string. + */ + public function substituteAttrEntities($string) + { + return preg_replace_callback( + $this->_attrEntitiesRegex, + array($this, 'entityCallback'), + $string + ); + } + + /** + * Callback function for substituteNonSpecialEntities() that does the work. + * + * @param array $matches PCRE matches array, with 0 the entire match, and + * either index 1, 2 or 3 set with a hex value, dec value, + * or string (respectively). + * @return string Replacement string. + */ + + protected function entityCallback($matches) + { + $entity = $matches[0]; + $hex_part = @$matches[1]; + $dec_part = @$matches[2]; + $named_part = empty($matches[3]) ? @$matches[4] : $matches[3]; + if ($hex_part !== NULL && $hex_part !== "") { + return HTMLPurifier_Encoder::unichr(hexdec($hex_part)); + } elseif ($dec_part !== NULL && $dec_part !== "") { + return HTMLPurifier_Encoder::unichr((int) $dec_part); + } else { + if (!$this->_entity_lookup) { + $this->_entity_lookup = HTMLPurifier_EntityLookup::instance(); + } + if (isset($this->_entity_lookup->table[$named_part])) { + return $this->_entity_lookup->table[$named_part]; + } else { + // exact match didn't match anything, so test if + // any of the semicolon optional match the prefix. + // Test that this is an EXACT match is important to + // prevent infinite loop + if (!empty($matches[3])) { + return preg_replace_callback( + $this->_semiOptionalPrefixRegex, + array($this, 'entityCallback'), + $entity + ); + } + return $entity; + } + } + } + + // LEGACY CODE BELOW + + /** + * Callback regex string for parsing entities. + * @type string + */ + protected $_substituteEntitiesRegex = + '/&(?:[#]x([a-fA-F0-9]+)|[#]0*(\d+)|([A-Za-z_:][A-Za-z0-9.\-_:]*));?/'; + // 1. hex 2. dec 3. string (XML style) + + /** + * Decimal to parsed string conversion table for special entities. + * @type array + */ + protected $_special_dec2str = + array( + 34 => '"', + 38 => '&', + 39 => "'", + 60 => '<', + 62 => '>' + ); + + /** + * Stripped entity names to decimal conversion table for special entities. + * @type array + */ + protected $_special_ent2dec = + array( + 'quot' => 34, + 'amp' => 38, + 'lt' => 60, + 'gt' => 62 + ); + + /** + * Substitutes non-special entities with their parsed equivalents. Since + * running this whenever you have parsed character is t3h 5uck, we run + * it before everything else. + * + * @param string $string String to have non-special entities parsed. + * @return string Parsed string. + */ + public function substituteNonSpecialEntities($string) + { + // it will try to detect missing semicolons, but don't rely on it + return preg_replace_callback( + $this->_substituteEntitiesRegex, + array($this, 'nonSpecialEntityCallback'), + $string + ); + } + + /** + * Callback function for substituteNonSpecialEntities() that does the work. + * + * @param array $matches PCRE matches array, with 0 the entire match, and + * either index 1, 2 or 3 set with a hex value, dec value, + * or string (respectively). + * @return string Replacement string. + */ + + protected function nonSpecialEntityCallback($matches) + { + // replaces all but big five + $entity = $matches[0]; + $is_num = (@$matches[0][1] === '#'); + if ($is_num) { + $is_hex = (@$entity[2] === 'x'); + $code = $is_hex ? hexdec($matches[1]) : (int) $matches[2]; + // abort for special characters + if (isset($this->_special_dec2str[$code])) { + return $entity; + } + return HTMLPurifier_Encoder::unichr($code); + } else { + if (isset($this->_special_ent2dec[$matches[3]])) { + return $entity; + } + if (!$this->_entity_lookup) { + $this->_entity_lookup = HTMLPurifier_EntityLookup::instance(); + } + if (isset($this->_entity_lookup->table[$matches[3]])) { + return $this->_entity_lookup->table[$matches[3]]; + } else { + return $entity; + } + } + } + + /** + * Substitutes only special entities with their parsed equivalents. + * + * @notice We try to avoid calling this function because otherwise, it + * would have to be called a lot (for every parsed section). + * + * @param string $string String to have non-special entities parsed. + * @return string Parsed string. + */ + public function substituteSpecialEntities($string) + { + return preg_replace_callback( + $this->_substituteEntitiesRegex, + array($this, 'specialEntityCallback'), + $string + ); + } + + /** + * Callback function for substituteSpecialEntities() that does the work. + * + * This callback has same syntax as nonSpecialEntityCallback(). + * + * @param array $matches PCRE-style matches array, with 0 the entire match, and + * either index 1, 2 or 3 set with a hex value, dec value, + * or string (respectively). + * @return string Replacement string. + */ + protected function specialEntityCallback($matches) + { + $entity = $matches[0]; + $is_num = (@$matches[0][1] === '#'); + if ($is_num) { + $is_hex = (@$entity[2] === 'x'); + $int = $is_hex ? hexdec($matches[1]) : (int) $matches[2]; + return isset($this->_special_dec2str[$int]) ? + $this->_special_dec2str[$int] : + $entity; + } else { + return isset($this->_special_ent2dec[$matches[3]]) ? + $this->_special_dec2str[$this->_special_ent2dec[$matches[3]]] : + $entity; + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorCollector.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorCollector.php new file mode 100644 index 000000000..d47e3f2e2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorCollector.php @@ -0,0 +1,244 @@ +<?php + +/** + * Error collection class that enables HTML Purifier to report HTML + * problems back to the user + */ +class HTMLPurifier_ErrorCollector +{ + + /** + * Identifiers for the returned error array. These are purposely numeric + * so list() can be used. + */ + const LINENO = 0; + const SEVERITY = 1; + const MESSAGE = 2; + const CHILDREN = 3; + + /** + * @type array + */ + protected $errors; + + /** + * @type array + */ + protected $_current; + + /** + * @type array + */ + protected $_stacks = array(array()); + + /** + * @type HTMLPurifier_Language + */ + protected $locale; + + /** + * @type HTMLPurifier_Generator + */ + protected $generator; + + /** + * @type HTMLPurifier_Context + */ + protected $context; + + /** + * @type array + */ + protected $lines = array(); + + /** + * @param HTMLPurifier_Context $context + */ + public function __construct($context) + { + $this->locale =& $context->get('Locale'); + $this->context = $context; + $this->_current =& $this->_stacks[0]; + $this->errors =& $this->_stacks[0]; + } + + /** + * Sends an error message to the collector for later use + * @param int $severity Error severity, PHP error style (don't use E_USER_) + * @param string $msg Error message text + */ + public function send($severity, $msg) + { + $args = array(); + if (func_num_args() > 2) { + $args = func_get_args(); + array_shift($args); + unset($args[0]); + } + + $token = $this->context->get('CurrentToken', true); + $line = $token ? $token->line : $this->context->get('CurrentLine', true); + $col = $token ? $token->col : $this->context->get('CurrentCol', true); + $attr = $this->context->get('CurrentAttr', true); + + // perform special substitutions, also add custom parameters + $subst = array(); + if (!is_null($token)) { + $args['CurrentToken'] = $token; + } + if (!is_null($attr)) { + $subst['$CurrentAttr.Name'] = $attr; + if (isset($token->attr[$attr])) { + $subst['$CurrentAttr.Value'] = $token->attr[$attr]; + } + } + + if (empty($args)) { + $msg = $this->locale->getMessage($msg); + } else { + $msg = $this->locale->formatMessage($msg, $args); + } + + if (!empty($subst)) { + $msg = strtr($msg, $subst); + } + + // (numerically indexed) + $error = array( + self::LINENO => $line, + self::SEVERITY => $severity, + self::MESSAGE => $msg, + self::CHILDREN => array() + ); + $this->_current[] = $error; + + // NEW CODE BELOW ... + // Top-level errors are either: + // TOKEN type, if $value is set appropriately, or + // "syntax" type, if $value is null + $new_struct = new HTMLPurifier_ErrorStruct(); + $new_struct->type = HTMLPurifier_ErrorStruct::TOKEN; + if ($token) { + $new_struct->value = clone $token; + } + if (is_int($line) && is_int($col)) { + if (isset($this->lines[$line][$col])) { + $struct = $this->lines[$line][$col]; + } else { + $struct = $this->lines[$line][$col] = $new_struct; + } + // These ksorts may present a performance problem + ksort($this->lines[$line], SORT_NUMERIC); + } else { + if (isset($this->lines[-1])) { + $struct = $this->lines[-1]; + } else { + $struct = $this->lines[-1] = $new_struct; + } + } + ksort($this->lines, SORT_NUMERIC); + + // Now, check if we need to operate on a lower structure + if (!empty($attr)) { + $struct = $struct->getChild(HTMLPurifier_ErrorStruct::ATTR, $attr); + if (!$struct->value) { + $struct->value = array($attr, 'PUT VALUE HERE'); + } + } + if (!empty($cssprop)) { + $struct = $struct->getChild(HTMLPurifier_ErrorStruct::CSSPROP, $cssprop); + if (!$struct->value) { + // if we tokenize CSS this might be a little more difficult to do + $struct->value = array($cssprop, 'PUT VALUE HERE'); + } + } + + // Ok, structs are all setup, now time to register the error + $struct->addError($severity, $msg); + } + + /** + * Retrieves raw error data for custom formatter to use + */ + public function getRaw() + { + return $this->errors; + } + + /** + * Default HTML formatting implementation for error messages + * @param HTMLPurifier_Config $config Configuration, vital for HTML output nature + * @param array $errors Errors array to display; used for recursion. + * @return string + */ + public function getHTMLFormatted($config, $errors = null) + { + $ret = array(); + + $this->generator = new HTMLPurifier_Generator($config, $this->context); + if ($errors === null) { + $errors = $this->errors; + } + + // 'At line' message needs to be removed + + // generation code for new structure goes here. It needs to be recursive. + foreach ($this->lines as $line => $col_array) { + if ($line == -1) { + continue; + } + foreach ($col_array as $col => $struct) { + $this->_renderStruct($ret, $struct, $line, $col); + } + } + if (isset($this->lines[-1])) { + $this->_renderStruct($ret, $this->lines[-1]); + } + + if (empty($errors)) { + return '<p>' . $this->locale->getMessage('ErrorCollector: No errors') . '</p>'; + } else { + return '<ul><li>' . implode('</li><li>', $ret) . '</li></ul>'; + } + + } + + private function _renderStruct(&$ret, $struct, $line = null, $col = null) + { + $stack = array($struct); + $context_stack = array(array()); + while ($current = array_pop($stack)) { + $context = array_pop($context_stack); + foreach ($current->errors as $error) { + list($severity, $msg) = $error; + $string = ''; + $string .= '<div>'; + // W3C uses an icon to indicate the severity of the error. + $error = $this->locale->getErrorName($severity); + $string .= "<span class=\"error e$severity\"><strong>$error</strong></span> "; + if (!is_null($line) && !is_null($col)) { + $string .= "<em class=\"location\">Line $line, Column $col: </em> "; + } else { + $string .= '<em class="location">End of Document: </em> '; + } + $string .= '<strong class="description">' . $this->generator->escape($msg) . '</strong> '; + $string .= '</div>'; + // Here, have a marker for the character on the column appropriate. + // Be sure to clip extremely long lines. + //$string .= '<pre>'; + //$string .= ''; + //$string .= '</pre>'; + $ret[] = $string; + } + foreach ($current->children as $array) { + $context[] = $current; + $stack = array_merge($stack, array_reverse($array, true)); + for ($i = count($array); $i > 0; $i--) { + $context_stack[] = $context; + } + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorStruct.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorStruct.php new file mode 100644 index 000000000..cf869d321 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorStruct.php @@ -0,0 +1,74 @@ +<?php + +/** + * Records errors for particular segments of an HTML document such as tokens, + * attributes or CSS properties. They can contain error structs (which apply + * to components of what they represent), but their main purpose is to hold + * errors applying to whatever struct is being used. + */ +class HTMLPurifier_ErrorStruct +{ + + /** + * Possible values for $children first-key. Note that top-level structures + * are automatically token-level. + */ + const TOKEN = 0; + const ATTR = 1; + const CSSPROP = 2; + + /** + * Type of this struct. + * @type string + */ + public $type; + + /** + * Value of the struct we are recording errors for. There are various + * values for this: + * - TOKEN: Instance of HTMLPurifier_Token + * - ATTR: array('attr-name', 'value') + * - CSSPROP: array('prop-name', 'value') + * @type mixed + */ + public $value; + + /** + * Errors registered for this structure. + * @type array + */ + public $errors = array(); + + /** + * Child ErrorStructs that are from this structure. For example, a TOKEN + * ErrorStruct would contain ATTR ErrorStructs. This is a multi-dimensional + * array in structure: [TYPE]['identifier'] + * @type array + */ + public $children = array(); + + /** + * @param string $type + * @param string $id + * @return mixed + */ + public function getChild($type, $id) + { + if (!isset($this->children[$type][$id])) { + $this->children[$type][$id] = new HTMLPurifier_ErrorStruct(); + $this->children[$type][$id]->type = $type; + } + return $this->children[$type][$id]; + } + + /** + * @param int $severity + * @param string $message + */ + public function addError($severity, $message) + { + $this->errors[] = array($severity, $message); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Exception.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Exception.php new file mode 100644 index 000000000..be85b4c56 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Exception.php @@ -0,0 +1,12 @@ +<?php + +/** + * Global exception class for HTML Purifier; any exceptions we throw + * are from here. + */ +class HTMLPurifier_Exception extends Exception +{ + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter.php new file mode 100644 index 000000000..c1f41ee16 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter.php @@ -0,0 +1,56 @@ +<?php + +/** + * Represents a pre or post processing filter on HTML Purifier's output + * + * Sometimes, a little ad-hoc fixing of HTML has to be done before + * it gets sent through HTML Purifier: you can use filters to acheive + * this effect. For instance, YouTube videos can be preserved using + * this manner. You could have used a decorator for this task, but + * PHP's support for them is not terribly robust, so we're going + * to just loop through the filters. + * + * Filters should be exited first in, last out. If there are three filters, + * named 1, 2 and 3, the order of execution should go 1->preFilter, + * 2->preFilter, 3->preFilter, purify, 3->postFilter, 2->postFilter, + * 1->postFilter. + * + * @note Methods are not declared abstract as it is perfectly legitimate + * for an implementation not to want anything to happen on a step + */ + +class HTMLPurifier_Filter +{ + + /** + * Name of the filter for identification purposes. + * @type string + */ + public $name; + + /** + * Pre-processor function, handles HTML before HTML Purifier + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function preFilter($html, $config, $context) + { + return $html; + } + + /** + * Post-processor function, handles HTML after HTML Purifier + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function postFilter($html, $config, $context) + { + return $html; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/ExtractStyleBlocks.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/ExtractStyleBlocks.php new file mode 100644 index 000000000..66f70b0fc --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/ExtractStyleBlocks.php @@ -0,0 +1,341 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/YouTube.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/YouTube.php new file mode 100644 index 000000000..276d8362f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/YouTube.php @@ -0,0 +1,65 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Generator.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Generator.php new file mode 100644 index 000000000..6fb568714 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Generator.php @@ -0,0 +1,286 @@ +<?php + +/** + * Generates HTML from tokens. + * @todo Refactor interface so that configuration/context is determined + * upon instantiation, no need for messy generateFromTokens() calls + * @todo Make some of the more internal functions protected, and have + * unit tests work around that + */ +class HTMLPurifier_Generator +{ + + /** + * Whether or not generator should produce XML output. + * @type bool + */ + private $_xhtml = true; + + /** + * :HACK: Whether or not generator should comment the insides of <script> tags. + * @type bool + */ + private $_scriptFix = false; + + /** + * Cache of HTMLDefinition during HTML output to determine whether or + * not attributes should be minimized. + * @type HTMLPurifier_HTMLDefinition + */ + private $_def; + + /** + * Cache of %Output.SortAttr. + * @type bool + */ + private $_sortAttr; + + /** + * Cache of %Output.FlashCompat. + * @type bool + */ + private $_flashCompat; + + /** + * Cache of %Output.FixInnerHTML. + * @type bool + */ + private $_innerHTMLFix; + + /** + * Stack for keeping track of object information when outputting IE + * compatibility code. + * @type array + */ + private $_flashStack = array(); + + /** + * Configuration for the generator + * @type HTMLPurifier_Config + */ + protected $config; + + /** + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + */ + public function __construct($config, $context) + { + $this->config = $config; + $this->_scriptFix = $config->get('Output.CommentScriptContents'); + $this->_innerHTMLFix = $config->get('Output.FixInnerHTML'); + $this->_sortAttr = $config->get('Output.SortAttr'); + $this->_flashCompat = $config->get('Output.FlashCompat'); + $this->_def = $config->getHTMLDefinition(); + $this->_xhtml = $this->_def->doctype->xml; + } + + /** + * Generates HTML from an array of tokens. + * @param HTMLPurifier_Token[] $tokens Array of HTMLPurifier_Token + * @return string Generated HTML + */ + public function generateFromTokens($tokens) + { + if (!$tokens) { + return ''; + } + + // Basic algorithm + $html = ''; + for ($i = 0, $size = count($tokens); $i < $size; $i++) { + if ($this->_scriptFix && $tokens[$i]->name === 'script' + && $i + 2 < $size && $tokens[$i+2] instanceof HTMLPurifier_Token_End) { + // script special case + // the contents of the script block must be ONE token + // for this to work. + $html .= $this->generateFromToken($tokens[$i++]); + $html .= $this->generateScriptFromToken($tokens[$i++]); + } + $html .= $this->generateFromToken($tokens[$i]); + } + + // Tidy cleanup + if (extension_loaded('tidy') && $this->config->get('Output.TidyFormat')) { + $tidy = new Tidy; + $tidy->parseString( + $html, + array( + 'indent'=> true, + 'output-xhtml' => $this->_xhtml, + 'show-body-only' => true, + 'indent-spaces' => 2, + 'wrap' => 68, + ), + 'utf8' + ); + $tidy->cleanRepair(); + $html = (string) $tidy; // explicit cast necessary + } + + // Normalize newlines to system defined value + if ($this->config->get('Core.NormalizeNewlines')) { + $nl = $this->config->get('Output.Newline'); + if ($nl === null) { + $nl = PHP_EOL; + } + if ($nl !== "\n") { + $html = str_replace("\n", $nl, $html); + } + } + return $html; + } + + /** + * Generates HTML from a single token. + * @param HTMLPurifier_Token $token HTMLPurifier_Token object. + * @return string Generated HTML + */ + public function generateFromToken($token) + { + if (!$token instanceof HTMLPurifier_Token) { + trigger_error('Cannot generate HTML from non-HTMLPurifier_Token object', E_USER_WARNING); + return ''; + + } elseif ($token instanceof HTMLPurifier_Token_Start) { + $attr = $this->generateAttributes($token->attr, $token->name); + if ($this->_flashCompat) { + if ($token->name == "object") { + $flash = new stdclass(); + $flash->attr = $token->attr; + $flash->param = array(); + $this->_flashStack[] = $flash; + } + } + return '<' . $token->name . ($attr ? ' ' : '') . $attr . '>'; + + } elseif ($token instanceof HTMLPurifier_Token_End) { + $_extra = ''; + if ($this->_flashCompat) { + if ($token->name == "object" && !empty($this->_flashStack)) { + // doesn't do anything for now + } + } + return $_extra . '</' . $token->name . '>'; + + } elseif ($token instanceof HTMLPurifier_Token_Empty) { + if ($this->_flashCompat && $token->name == "param" && !empty($this->_flashStack)) { + $this->_flashStack[count($this->_flashStack)-1]->param[$token->attr['name']] = $token->attr['value']; + } + $attr = $this->generateAttributes($token->attr, $token->name); + return '<' . $token->name . ($attr ? ' ' : '') . $attr . + ( $this->_xhtml ? ' /': '' ) // <br /> v. <br> + . '>'; + + } elseif ($token instanceof HTMLPurifier_Token_Text) { + return $this->escape($token->data, ENT_NOQUOTES); + + } elseif ($token instanceof HTMLPurifier_Token_Comment) { + return '<!--' . $token->data . '-->'; + } else { + return ''; + + } + } + + /** + * Special case processor for the contents of script tags + * @param HTMLPurifier_Token $token HTMLPurifier_Token object. + * @return string + * @warning This runs into problems if there's already a literal + * --> somewhere inside the script contents. + */ + public function generateScriptFromToken($token) + { + if (!$token instanceof HTMLPurifier_Token_Text) { + return $this->generateFromToken($token); + } + // Thanks <http://lachy.id.au/log/2005/05/script-comments> + $data = preg_replace('#//\s*$#', '', $token->data); + return '<!--//--><![CDATA[//><!--' . "\n" . trim($data) . "\n" . '//--><!]]>'; + } + + /** + * Generates attribute declarations from attribute array. + * @note This does not include the leading or trailing space. + * @param array $assoc_array_of_attributes Attribute array + * @param string $element Name of element attributes are for, used to check + * attribute minimization. + * @return string Generated HTML fragment for insertion. + */ + public function generateAttributes($assoc_array_of_attributes, $element = '') + { + $html = ''; + if ($this->_sortAttr) { + ksort($assoc_array_of_attributes); + } + foreach ($assoc_array_of_attributes as $key => $value) { + if (!$this->_xhtml) { + // Remove namespaced attributes + if (strpos($key, ':') !== false) { + continue; + } + // Check if we should minimize the attribute: val="val" -> val + if ($element && !empty($this->_def->info[$element]->attr[$key]->minimized)) { + $html .= $key . ' '; + continue; + } + } + // Workaround for Internet Explorer innerHTML bug. + // Essentially, Internet Explorer, when calculating + // innerHTML, omits quotes if there are no instances of + // angled brackets, quotes or spaces. However, when parsing + // HTML (for example, when you assign to innerHTML), it + // treats backticks as quotes. Thus, + // <img alt="``" /> + // becomes + // <img alt=`` /> + // becomes + // <img alt='' /> + // Fortunately, all we need to do is trigger an appropriate + // quoting style, which we do by adding an extra space. + // This also is consistent with the W3C spec, which states + // that user agents may ignore leading or trailing + // whitespace (in fact, most don't, at least for attributes + // like alt, but an extra space at the end is barely + // noticeable). Still, we have a configuration knob for + // this, since this transformation is not necesary if you + // don't process user input with innerHTML or you don't plan + // on supporting Internet Explorer. + if ($this->_innerHTMLFix) { + if (strpos($value, '`') !== false) { + // check if correct quoting style would not already be + // triggered + if (strcspn($value, '"\' <>') === strlen($value)) { + // protect! + $value .= ' '; + } + } + } + $html .= $key.'="'.$this->escape($value).'" '; + } + return rtrim($html); + } + + /** + * Escapes raw text data. + * @todo This really ought to be protected, but until we have a facility + * for properly generating HTML here w/o using tokens, it stays + * public. + * @param string $string String data to escape for HTML. + * @param int $quote Quoting style, like htmlspecialchars. ENT_NOQUOTES is + * permissible for non-attribute output. + * @return string escaped data. + */ + public function escape($string, $quote = null) + { + // Workaround for APC bug on Mac Leopard reported by sidepodcast + // http://htmlpurifier.org/phorum/read.php?3,4823,4846 + if ($quote === null) { + $quote = ENT_COMPAT; + } + return htmlspecialchars($string, $quote, 'UTF-8'); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLDefinition.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLDefinition.php new file mode 100644 index 000000000..9b7b334dd --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLDefinition.php @@ -0,0 +1,493 @@ +<?php + +/** + * Definition of the purified HTML that describes allowed children, + * attributes, and many other things. + * + * Conventions: + * + * All member variables that are prefixed with info + * (including the main $info array) are used by HTML Purifier internals + * and should not be directly edited when customizing the HTMLDefinition. + * They can usually be set via configuration directives or custom + * modules. + * + * On the other hand, member variables without the info prefix are used + * internally by the HTMLDefinition and MUST NOT be used by other HTML + * Purifier internals. Many of them, however, are public, and may be + * edited by userspace code to tweak the behavior of HTMLDefinition. + * + * @note This class is inspected by Printer_HTMLDefinition; please + * update that class if things here change. + * + * @warning Directives that change this object's structure must be in + * the HTML or Attr namespace! + */ +class HTMLPurifier_HTMLDefinition extends HTMLPurifier_Definition +{ + + // FULLY-PUBLIC VARIABLES --------------------------------------------- + + /** + * Associative array of element names to HTMLPurifier_ElementDef. + * @type HTMLPurifier_ElementDef[] + */ + public $info = array(); + + /** + * Associative array of global attribute name to attribute definition. + * @type array + */ + public $info_global_attr = array(); + + /** + * String name of parent element HTML will be going into. + * @type string + */ + public $info_parent = 'div'; + + /** + * Definition for parent element, allows parent element to be a + * tag that's not allowed inside the HTML fragment. + * @type HTMLPurifier_ElementDef + */ + public $info_parent_def; + + /** + * String name of element used to wrap inline elements in block context. + * @type string + * @note This is rarely used except for BLOCKQUOTEs in strict mode + */ + public $info_block_wrapper = 'p'; + + /** + * Associative array of deprecated tag name to HTMLPurifier_TagTransform. + * @type array + */ + public $info_tag_transform = array(); + + /** + * Indexed list of HTMLPurifier_AttrTransform to be performed before validation. + * @type HTMLPurifier_AttrTransform[] + */ + public $info_attr_transform_pre = array(); + + /** + * Indexed list of HTMLPurifier_AttrTransform to be performed after validation. + * @type HTMLPurifier_AttrTransform[] + */ + public $info_attr_transform_post = array(); + + /** + * Nested lookup array of content set name (Block, Inline) to + * element name to whether or not it belongs in that content set. + * @type array + */ + public $info_content_sets = array(); + + /** + * Indexed list of HTMLPurifier_Injector to be used. + * @type HTMLPurifier_Injector[] + */ + public $info_injector = array(); + + /** + * Doctype object + * @type HTMLPurifier_Doctype + */ + public $doctype; + + + + // RAW CUSTOMIZATION STUFF -------------------------------------------- + + /** + * Adds a custom attribute to a pre-existing element + * @note This is strictly convenience, and does not have a corresponding + * method in HTMLPurifier_HTMLModule + * @param string $element_name Element name to add attribute to + * @param string $attr_name Name of attribute + * @param mixed $def Attribute definition, can be string or object, see + * HTMLPurifier_AttrTypes for details + */ + public function addAttribute($element_name, $attr_name, $def) + { + $module = $this->getAnonymousModule(); + if (!isset($module->info[$element_name])) { + $element = $module->addBlankElement($element_name); + } else { + $element = $module->info[$element_name]; + } + $element->attr[$attr_name] = $def; + } + + /** + * Adds a custom element to your HTML definition + * @see HTMLPurifier_HTMLModule::addElement() for detailed + * parameter and return value descriptions. + */ + public function addElement($element_name, $type, $contents, $attr_collections, $attributes = array()) + { + $module = $this->getAnonymousModule(); + // assume that if the user is calling this, the element + // is safe. This may not be a good idea + $element = $module->addElement($element_name, $type, $contents, $attr_collections, $attributes); + return $element; + } + + /** + * Adds a blank element to your HTML definition, for overriding + * existing behavior + * @param string $element_name + * @return HTMLPurifier_ElementDef + * @see HTMLPurifier_HTMLModule::addBlankElement() for detailed + * parameter and return value descriptions. + */ + public function addBlankElement($element_name) + { + $module = $this->getAnonymousModule(); + $element = $module->addBlankElement($element_name); + return $element; + } + + /** + * Retrieves a reference to the anonymous module, so you can + * bust out advanced features without having to make your own + * module. + * @return HTMLPurifier_HTMLModule + */ + public function getAnonymousModule() + { + if (!$this->_anonModule) { + $this->_anonModule = new HTMLPurifier_HTMLModule(); + $this->_anonModule->name = 'Anonymous'; + } + return $this->_anonModule; + } + + private $_anonModule = null; + + // PUBLIC BUT INTERNAL VARIABLES -------------------------------------- + + /** + * @type string + */ + public $type = 'HTML'; + + /** + * @type HTMLPurifier_HTMLModuleManager + */ + public $manager; + + /** + * Performs low-cost, preliminary initialization. + */ + public function __construct() + { + $this->manager = new HTMLPurifier_HTMLModuleManager(); + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetup($config) + { + $this->processModules($config); + $this->setupConfigStuff($config); + unset($this->manager); + + // cleanup some of the element definitions + foreach ($this->info as $k => $v) { + unset($this->info[$k]->content_model); + unset($this->info[$k]->content_model_type); + } + } + + /** + * Extract out the information from the manager + * @param HTMLPurifier_Config $config + */ + protected function processModules($config) + { + if ($this->_anonModule) { + // for user specific changes + // this is late-loaded so we don't have to deal with PHP4 + // reference wonky-ness + $this->manager->addModule($this->_anonModule); + unset($this->_anonModule); + } + + $this->manager->setup($config); + $this->doctype = $this->manager->doctype; + + foreach ($this->manager->modules as $module) { + foreach ($module->info_tag_transform as $k => $v) { + if ($v === false) { + unset($this->info_tag_transform[$k]); + } else { + $this->info_tag_transform[$k] = $v; + } + } + foreach ($module->info_attr_transform_pre as $k => $v) { + if ($v === false) { + unset($this->info_attr_transform_pre[$k]); + } else { + $this->info_attr_transform_pre[$k] = $v; + } + } + foreach ($module->info_attr_transform_post as $k => $v) { + if ($v === false) { + unset($this->info_attr_transform_post[$k]); + } else { + $this->info_attr_transform_post[$k] = $v; + } + } + foreach ($module->info_injector as $k => $v) { + if ($v === false) { + unset($this->info_injector[$k]); + } else { + $this->info_injector[$k] = $v; + } + } + } + $this->info = $this->manager->getElements(); + $this->info_content_sets = $this->manager->contentSets->lookup; + } + + /** + * Sets up stuff based on config. We need a better way of doing this. + * @param HTMLPurifier_Config $config + */ + protected function setupConfigStuff($config) + { + $block_wrapper = $config->get('HTML.BlockWrapper'); + if (isset($this->info_content_sets['Block'][$block_wrapper])) { + $this->info_block_wrapper = $block_wrapper; + } else { + trigger_error( + 'Cannot use non-block element as block wrapper', + E_USER_ERROR + ); + } + + $parent = $config->get('HTML.Parent'); + $def = $this->manager->getElement($parent, true); + if ($def) { + $this->info_parent = $parent; + $this->info_parent_def = $def; + } else { + trigger_error( + 'Cannot use unrecognized element as parent', + E_USER_ERROR + ); + $this->info_parent_def = $this->manager->getElement($this->info_parent, true); + } + + // support template text + $support = "(for information on implementing this, see the support forums) "; + + // setup allowed elements ----------------------------------------- + + $allowed_elements = $config->get('HTML.AllowedElements'); + $allowed_attributes = $config->get('HTML.AllowedAttributes'); // retrieve early + + if (!is_array($allowed_elements) && !is_array($allowed_attributes)) { + $allowed = $config->get('HTML.Allowed'); + if (is_string($allowed)) { + list($allowed_elements, $allowed_attributes) = $this->parseTinyMCEAllowedList($allowed); + } + } + + if (is_array($allowed_elements)) { + foreach ($this->info as $name => $d) { + if (!isset($allowed_elements[$name])) { + unset($this->info[$name]); + } + unset($allowed_elements[$name]); + } + // emit errors + foreach ($allowed_elements as $element => $d) { + $element = htmlspecialchars($element); // PHP doesn't escape errors, be careful! + trigger_error("Element '$element' is not supported $support", E_USER_WARNING); + } + } + + // setup allowed attributes --------------------------------------- + + $allowed_attributes_mutable = $allowed_attributes; // by copy! + if (is_array($allowed_attributes)) { + // This actually doesn't do anything, since we went away from + // global attributes. It's possible that userland code uses + // it, but HTMLModuleManager doesn't! + foreach ($this->info_global_attr as $attr => $x) { + $keys = array($attr, "*@$attr", "*.$attr"); + $delete = true; + foreach ($keys as $key) { + if ($delete && isset($allowed_attributes[$key])) { + $delete = false; + } + if (isset($allowed_attributes_mutable[$key])) { + unset($allowed_attributes_mutable[$key]); + } + } + if ($delete) { + unset($this->info_global_attr[$attr]); + } + } + + foreach ($this->info as $tag => $info) { + foreach ($info->attr as $attr => $x) { + $keys = array("$tag@$attr", $attr, "*@$attr", "$tag.$attr", "*.$attr"); + $delete = true; + foreach ($keys as $key) { + if ($delete && isset($allowed_attributes[$key])) { + $delete = false; + } + if (isset($allowed_attributes_mutable[$key])) { + unset($allowed_attributes_mutable[$key]); + } + } + if ($delete) { + if ($this->info[$tag]->attr[$attr]->required) { + trigger_error( + "Required attribute '$attr' in element '$tag' " . + "was not allowed, which means '$tag' will not be allowed either", + E_USER_WARNING + ); + } + unset($this->info[$tag]->attr[$attr]); + } + } + } + // emit errors + foreach ($allowed_attributes_mutable as $elattr => $d) { + $bits = preg_split('/[.@]/', $elattr, 2); + $c = count($bits); + switch ($c) { + case 2: + if ($bits[0] !== '*') { + $element = htmlspecialchars($bits[0]); + $attribute = htmlspecialchars($bits[1]); + if (!isset($this->info[$element])) { + trigger_error( + "Cannot allow attribute '$attribute' if element " . + "'$element' is not allowed/supported $support" + ); + } else { + trigger_error( + "Attribute '$attribute' in element '$element' not supported $support", + E_USER_WARNING + ); + } + break; + } + // otherwise fall through + case 1: + $attribute = htmlspecialchars($bits[0]); + trigger_error( + "Global attribute '$attribute' is not ". + "supported in any elements $support", + E_USER_WARNING + ); + break; + } + } + } + + // setup forbidden elements --------------------------------------- + + $forbidden_elements = $config->get('HTML.ForbiddenElements'); + $forbidden_attributes = $config->get('HTML.ForbiddenAttributes'); + + foreach ($this->info as $tag => $info) { + if (isset($forbidden_elements[$tag])) { + unset($this->info[$tag]); + continue; + } + foreach ($info->attr as $attr => $x) { + if (isset($forbidden_attributes["$tag@$attr"]) || + isset($forbidden_attributes["*@$attr"]) || + isset($forbidden_attributes[$attr]) + ) { + unset($this->info[$tag]->attr[$attr]); + continue; + } elseif (isset($forbidden_attributes["$tag.$attr"])) { // this segment might get removed eventually + // $tag.$attr are not user supplied, so no worries! + trigger_error( + "Error with $tag.$attr: tag.attr syntax not supported for " . + "HTML.ForbiddenAttributes; use tag@attr instead", + E_USER_WARNING + ); + } + } + } + foreach ($forbidden_attributes as $key => $v) { + if (strlen($key) < 2) { + continue; + } + if ($key[0] != '*') { + continue; + } + if ($key[1] == '.') { + trigger_error( + "Error with $key: *.attr syntax not supported for HTML.ForbiddenAttributes; use attr instead", + E_USER_WARNING + ); + } + } + + // setup injectors ----------------------------------------------------- + foreach ($this->info_injector as $i => $injector) { + if ($injector->checkNeeded($config) !== false) { + // remove injector that does not have it's required + // elements/attributes present, and is thus not needed. + unset($this->info_injector[$i]); + } + } + } + + /** + * Parses a TinyMCE-flavored Allowed Elements and Attributes list into + * separate lists for processing. Format is element[attr1|attr2],element2... + * @warning Although it's largely drawn from TinyMCE's implementation, + * it is different, and you'll probably have to modify your lists + * @param array $list String list to parse + * @return array + * @todo Give this its own class, probably static interface + */ + public function parseTinyMCEAllowedList($list) + { + $list = str_replace(array(' ', "\t"), '', $list); + + $elements = array(); + $attributes = array(); + + $chunks = preg_split('/(,|[\n\r]+)/', $list); + foreach ($chunks as $chunk) { + if (empty($chunk)) { + continue; + } + // remove TinyMCE element control characters + if (!strpos($chunk, '[')) { + $element = $chunk; + $attr = false; + } else { + list($element, $attr) = explode('[', $chunk); + } + if ($element !== '*') { + $elements[$element] = true; + } + if (!$attr) { + continue; + } + $attr = substr($attr, 0, strlen($attr) - 1); // remove trailing ] + $attr = explode('|', $attr); + foreach ($attr as $key) { + $attributes["$element.$key"] = true; + } + } + return array($elements, $attributes); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule.php new file mode 100644 index 000000000..bb3a9230b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule.php @@ -0,0 +1,284 @@ +<?php + +/** + * Represents an XHTML 1.1 module, with information on elements, tags + * and attributes. + * @note Even though this is technically XHTML 1.1, it is also used for + * regular HTML parsing. We are using modulization as a convenient + * way to represent the internals of HTMLDefinition, and our + * implementation is by no means conforming and does not directly + * use the normative DTDs or XML schemas. + * @note The public variables in a module should almost directly + * correspond to the variables in HTMLPurifier_HTMLDefinition. + * However, the prefix info carries no special meaning in these + * objects (include it anyway if that's the correspondence though). + * @todo Consider making some member functions protected + */ + +class HTMLPurifier_HTMLModule +{ + + // -- Overloadable ---------------------------------------------------- + + /** + * Short unique string identifier of the module. + * @type string + */ + public $name; + + /** + * Informally, a list of elements this module changes. + * Not used in any significant way. + * @type array + */ + public $elements = array(); + + /** + * Associative array of element names to element definitions. + * Some definitions may be incomplete, to be merged in later + * with the full definition. + * @type array + */ + public $info = array(); + + /** + * Associative array of content set names to content set additions. + * This is commonly used to, say, add an A element to the Inline + * content set. This corresponds to an internal variable $content_sets + * and NOT info_content_sets member variable of HTMLDefinition. + * @type array + */ + public $content_sets = array(); + + /** + * Associative array of attribute collection names to attribute + * collection additions. More rarely used for adding attributes to + * the global collections. Example is the StyleAttribute module adding + * the style attribute to the Core. Corresponds to HTMLDefinition's + * attr_collections->info, since the object's data is only info, + * with extra behavior associated with it. + * @type array + */ + public $attr_collections = array(); + + /** + * Associative array of deprecated tag name to HTMLPurifier_TagTransform. + * @type array + */ + public $info_tag_transform = array(); + + /** + * List of HTMLPurifier_AttrTransform to be performed before validation. + * @type array + */ + public $info_attr_transform_pre = array(); + + /** + * List of HTMLPurifier_AttrTransform to be performed after validation. + * @type array + */ + public $info_attr_transform_post = array(); + + /** + * List of HTMLPurifier_Injector to be performed during well-formedness fixing. + * An injector will only be invoked if all of it's pre-requisites are met; + * if an injector fails setup, there will be no error; it will simply be + * silently disabled. + * @type array + */ + public $info_injector = array(); + + /** + * Boolean flag that indicates whether or not getChildDef is implemented. + * For optimization reasons: may save a call to a function. Be sure + * to set it if you do implement getChildDef(), otherwise it will have + * no effect! + * @type bool + */ + public $defines_child_def = false; + + /** + * Boolean flag whether or not this module is safe. If it is not safe, all + * of its members are unsafe. Modules are safe by default (this might be + * slightly dangerous, but it doesn't make much sense to force HTML Purifier, + * which is based off of safe HTML, to explicitly say, "This is safe," even + * though there are modules which are "unsafe") + * + * @type bool + * @note Previously, safety could be applied at an element level granularity. + * We've removed this ability, so in order to add "unsafe" elements + * or attributes, a dedicated module with this property set to false + * must be used. + */ + public $safe = true; + + /** + * Retrieves a proper HTMLPurifier_ChildDef subclass based on + * content_model and content_model_type member variables of + * the HTMLPurifier_ElementDef class. There is a similar function + * in HTMLPurifier_HTMLDefinition. + * @param HTMLPurifier_ElementDef $def + * @return HTMLPurifier_ChildDef subclass + */ + public function getChildDef($def) + { + return false; + } + + // -- Convenience ----------------------------------------------------- + + /** + * Convenience function that sets up a new element + * @param string $element Name of element to add + * @param string|bool $type What content set should element be registered to? + * Set as false to skip this step. + * @param string $contents Allowed children in form of: + * "$content_model_type: $content_model" + * @param array $attr_includes What attribute collections to register to + * element? + * @param array $attr What unique attributes does the element define? + * @see HTMLPurifier_ElementDef:: for in-depth descriptions of these parameters. + * @return HTMLPurifier_ElementDef Created element definition object, so you + * can set advanced parameters + */ + public function addElement($element, $type, $contents, $attr_includes = array(), $attr = array()) + { + $this->elements[] = $element; + // parse content_model + list($content_model_type, $content_model) = $this->parseContents($contents); + // merge in attribute inclusions + $this->mergeInAttrIncludes($attr, $attr_includes); + // add element to content sets + if ($type) { + $this->addElementToContentSet($element, $type); + } + // create element + $this->info[$element] = HTMLPurifier_ElementDef::create( + $content_model, + $content_model_type, + $attr + ); + // literal object $contents means direct child manipulation + if (!is_string($contents)) { + $this->info[$element]->child = $contents; + } + return $this->info[$element]; + } + + /** + * Convenience function that creates a totally blank, non-standalone + * element. + * @param string $element Name of element to create + * @return HTMLPurifier_ElementDef Created element + */ + public function addBlankElement($element) + { + if (!isset($this->info[$element])) { + $this->elements[] = $element; + $this->info[$element] = new HTMLPurifier_ElementDef(); + $this->info[$element]->standalone = false; + } else { + trigger_error("Definition for $element already exists in module, cannot redefine"); + } + return $this->info[$element]; + } + + /** + * Convenience function that registers an element to a content set + * @param string $element Element to register + * @param string $type Name content set (warning: case sensitive, usually upper-case + * first letter) + */ + public function addElementToContentSet($element, $type) + { + if (!isset($this->content_sets[$type])) { + $this->content_sets[$type] = ''; + } else { + $this->content_sets[$type] .= ' | '; + } + $this->content_sets[$type] .= $element; + } + + /** + * Convenience function that transforms single-string contents + * into separate content model and content model type + * @param string $contents Allowed children in form of: + * "$content_model_type: $content_model" + * @return array + * @note If contents is an object, an array of two nulls will be + * returned, and the callee needs to take the original $contents + * and use it directly. + */ + public function parseContents($contents) + { + if (!is_string($contents)) { + return array(null, null); + } // defer + switch ($contents) { + // check for shorthand content model forms + case 'Empty': + return array('empty', ''); + case 'Inline': + return array('optional', 'Inline | #PCDATA'); + case 'Flow': + return array('optional', 'Flow | #PCDATA'); + } + list($content_model_type, $content_model) = explode(':', $contents); + $content_model_type = strtolower(trim($content_model_type)); + $content_model = trim($content_model); + return array($content_model_type, $content_model); + } + + /** + * Convenience function that merges a list of attribute includes into + * an attribute array. + * @param array $attr Reference to attr array to modify + * @param array $attr_includes Array of includes / string include to merge in + */ + public function mergeInAttrIncludes(&$attr, $attr_includes) + { + if (!is_array($attr_includes)) { + if (empty($attr_includes)) { + $attr_includes = array(); + } else { + $attr_includes = array($attr_includes); + } + } + $attr[0] = $attr_includes; + } + + /** + * Convenience function that generates a lookup table with boolean + * true as value. + * @param string $list List of values to turn into a lookup + * @note You can also pass an arbitrary number of arguments in + * place of the regular argument + * @return array array equivalent of list + */ + public function makeLookup($list) + { + if (is_string($list)) { + $list = func_get_args(); + } + $ret = array(); + foreach ($list as $value) { + if (is_null($value)) { + continue; + } + $ret[$value] = true; + } + return $ret; + } + + /** + * Lazy load construction of the module after determining whether + * or not it's needed, and also when a finalized configuration object + * is available. + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Bdo.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Bdo.php new file mode 100644 index 000000000..1e67c790d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Bdo.php @@ -0,0 +1,44 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/CommonAttributes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/CommonAttributes.php new file mode 100644 index 000000000..a96ab1bef --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/CommonAttributes.php @@ -0,0 +1,31 @@ +<?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', + ), + 'Lang' => array(), + 'I18N' => array( + 0 => array('Lang'), // proprietary, for xml:lang/lang + ), + 'Common' => array( + 0 => array('Core', 'I18N') + ) + ); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Edit.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Edit.php new file mode 100644 index 000000000..a9042a357 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Edit.php @@ -0,0 +1,55 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Forms.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Forms.php new file mode 100644 index 000000000..6f7ddbc05 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Forms.php @@ -0,0 +1,190 @@ +<?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) + { + $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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Hypertext.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Hypertext.php new file mode 100644 index 000000000..72d7a31e6 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Hypertext.php @@ -0,0 +1,40 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Iframe.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Iframe.php new file mode 100644 index 000000000..f7e7c91c0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Iframe.php @@ -0,0 +1,51 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Image.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Image.php new file mode 100644 index 000000000..0f5fdb3ba --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Image.php @@ -0,0 +1,49 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Legacy.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Legacy.php new file mode 100644 index 000000000..86b529957 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Legacy.php @@ -0,0 +1,186 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/List.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/List.php new file mode 100644 index 000000000..7a20ff701 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/List.php @@ -0,0 +1,51 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Name.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Name.php new file mode 100644 index 000000000..60c054515 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Name.php @@ -0,0 +1,26 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Nofollow.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Nofollow.php new file mode 100644 index 000000000..dc9410a89 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Nofollow.php @@ -0,0 +1,25 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php new file mode 100644 index 000000000..da722253a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php @@ -0,0 +1,20 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Object.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Object.php new file mode 100644 index 000000000..2f9efc5c8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Object.php @@ -0,0 +1,62 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Presentation.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Presentation.php new file mode 100644 index 000000000..6458ce9d8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Presentation.php @@ -0,0 +1,42 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Proprietary.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Proprietary.php new file mode 100644 index 000000000..5ee3c8e67 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Proprietary.php @@ -0,0 +1,40 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Ruby.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Ruby.php new file mode 100644 index 000000000..a0d48924d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Ruby.php @@ -0,0 +1,36 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeEmbed.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeEmbed.php new file mode 100644 index 000000000..04e6689ea --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeEmbed.php @@ -0,0 +1,40 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeObject.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeObject.php new file mode 100644 index 000000000..1297f80a3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeObject.php @@ -0,0 +1,62 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeScripting.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeScripting.php new file mode 100644 index 000000000..0330cd97f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeScripting.php @@ -0,0 +1,40 @@ +<?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', + 'Empty', + 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)) + ) + ); + $script->attr_transform_pre[] = + $script->attr_transform_post[] = new HTMLPurifier_AttrTransform_ScriptRequired(); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Scripting.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Scripting.php new file mode 100644 index 000000000..8b28a7b7e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Scripting.php @@ -0,0 +1,73 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/StyleAttribute.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/StyleAttribute.php new file mode 100644 index 000000000..497b832ae --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/StyleAttribute.php @@ -0,0 +1,33 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tables.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tables.php new file mode 100644 index 000000000..8a0b3b461 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tables.php @@ -0,0 +1,75 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Target.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Target.php new file mode 100644 index 000000000..b188ac936 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Target.php @@ -0,0 +1,28 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetBlank.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetBlank.php new file mode 100644 index 000000000..58ccc6894 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetBlank.php @@ -0,0 +1,24 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoopener.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoopener.php new file mode 100644 index 000000000..b967ff566 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoopener.php @@ -0,0 +1,21 @@ +<?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(); + } +} diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoreferrer.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoreferrer.php new file mode 100644 index 000000000..32484d601 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoreferrer.php @@ -0,0 +1,21 @@ +<?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(); + } +} diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Text.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Text.php new file mode 100644 index 000000000..7a65e0048 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Text.php @@ -0,0 +1,87 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy.php new file mode 100644 index 000000000..08aa23247 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy.php @@ -0,0 +1,230 @@ +<?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; + } + // PHP does some weird parsing when I do + // $e->$type[$attr], so I have to assign a ref. + $f =& $e->$type; + $f[$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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Name.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Name.php new file mode 100644 index 000000000..a995161b2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Name.php @@ -0,0 +1,33 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Proprietary.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Proprietary.php new file mode 100644 index 000000000..332643821 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Proprietary.php @@ -0,0 +1,34 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Strict.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Strict.php new file mode 100644 index 000000000..803c44fab --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Strict.php @@ -0,0 +1,43 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Transitional.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Transitional.php new file mode 100644 index 000000000..c095ad974 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Transitional.php @@ -0,0 +1,16 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/XHTML.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/XHTML.php new file mode 100644 index 000000000..3ecddc434 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/XHTML.php @@ -0,0 +1,26 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php new file mode 100644 index 000000000..c4f16a4dc --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php @@ -0,0 +1,179 @@ +<?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['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 hr, td, th ------------------------------------------ + $r['td@width'] = + $r['th@width'] = + $r['hr@width'] = new HTMLPurifier_AttrTransform_Length('width'); + + return $r; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/XMLCommonAttributes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/XMLCommonAttributes.php new file mode 100644 index 000000000..01dbe9deb --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/XMLCommonAttributes.php @@ -0,0 +1,20 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModuleManager.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModuleManager.php new file mode 100644 index 000000000..38c058fe2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModuleManager.php @@ -0,0 +1,467 @@ +<?php + +class HTMLPurifier_HTMLModuleManager +{ + + /** + * @type HTMLPurifier_DoctypeRegistry + */ + public $doctypes; + + /** + * Instance of current doctype. + * @type string + */ + public $doctype; + + /** + * @type HTMLPurifier_AttrTypes + */ + public $attrTypes; + + /** + * Active instances of modules for the specified doctype are + * indexed, by name, in this array. + * @type HTMLPurifier_HTMLModule[] + */ + public $modules = array(); + + /** + * Array of recognized HTMLPurifier_HTMLModule instances, + * indexed by module's class name. This array is usually lazy loaded, but a + * user can overload a module by pre-emptively registering it. + * @type HTMLPurifier_HTMLModule[] + */ + public $registeredModules = array(); + + /** + * List of extra modules that were added by the user + * using addModule(). These get unconditionally merged into the current doctype, whatever + * it may be. + * @type HTMLPurifier_HTMLModule[] + */ + public $userModules = array(); + + /** + * Associative array of element name to list of modules that have + * definitions for the element; this array is dynamically filled. + * @type array + */ + public $elementLookup = array(); + + /** + * List of prefixes we should use for registering small names. + * @type array + */ + public $prefixes = array('HTMLPurifier_HTMLModule_'); + + /** + * @type HTMLPurifier_ContentSets + */ + public $contentSets; + + /** + * @type HTMLPurifier_AttrCollections + */ + public $attrCollections; + + /** + * If set to true, unsafe elements and attributes will be allowed. + * @type bool + */ + public $trusted = false; + + public function __construct() + { + // editable internal objects + $this->attrTypes = new HTMLPurifier_AttrTypes(); + $this->doctypes = new HTMLPurifier_DoctypeRegistry(); + + // setup basic modules + $common = array( + 'CommonAttributes', 'Text', 'Hypertext', 'List', + 'Presentation', 'Edit', 'Bdo', 'Tables', 'Image', + 'StyleAttribute', + // Unsafe: + 'Scripting', 'Object', 'Forms', + // Sorta legacy, but present in strict: + 'Name', + ); + $transitional = array('Legacy', 'Target', 'Iframe'); + $xml = array('XMLCommonAttributes'); + $non_xml = array('NonXMLCommonAttributes'); + + // setup basic doctypes + $this->doctypes->register( + 'HTML 4.01 Transitional', + false, + array_merge($common, $transitional, $non_xml), + array('Tidy_Transitional', 'Tidy_Proprietary'), + array(), + '-//W3C//DTD HTML 4.01 Transitional//EN', + 'http://www.w3.org/TR/html4/loose.dtd' + ); + + $this->doctypes->register( + 'HTML 4.01 Strict', + false, + array_merge($common, $non_xml), + array('Tidy_Strict', 'Tidy_Proprietary', 'Tidy_Name'), + array(), + '-//W3C//DTD HTML 4.01//EN', + 'http://www.w3.org/TR/html4/strict.dtd' + ); + + $this->doctypes->register( + 'XHTML 1.0 Transitional', + true, + array_merge($common, $transitional, $xml, $non_xml), + array('Tidy_Transitional', 'Tidy_XHTML', 'Tidy_Proprietary', 'Tidy_Name'), + array(), + '-//W3C//DTD XHTML 1.0 Transitional//EN', + 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd' + ); + + $this->doctypes->register( + 'XHTML 1.0 Strict', + true, + array_merge($common, $xml, $non_xml), + array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_Strict', 'Tidy_Proprietary', 'Tidy_Name'), + array(), + '-//W3C//DTD XHTML 1.0 Strict//EN', + 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd' + ); + + $this->doctypes->register( + 'XHTML 1.1', + true, + // Iframe is a real XHTML 1.1 module, despite being + // "transitional"! + array_merge($common, $xml, array('Ruby', 'Iframe')), + array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_Proprietary', 'Tidy_Strict', 'Tidy_Name'), // Tidy_XHTML1_1 + array(), + '-//W3C//DTD XHTML 1.1//EN', + 'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd' + ); + + } + + /** + * Registers a module to the recognized module list, useful for + * overloading pre-existing modules. + * @param $module Mixed: string module name, with or without + * HTMLPurifier_HTMLModule prefix, or instance of + * subclass of HTMLPurifier_HTMLModule. + * @param $overload Boolean whether or not to overload previous modules. + * If this is not set, and you do overload a module, + * HTML Purifier will complain with a warning. + * @note This function will not call autoload, you must instantiate + * (and thus invoke) autoload outside the method. + * @note If a string is passed as a module name, different variants + * will be tested in this order: + * - Check for HTMLPurifier_HTMLModule_$name + * - Check all prefixes with $name in order they were added + * - Check for literal object name + * - Throw fatal error + * If your object name collides with an internal class, specify + * your module manually. All modules must have been included + * externally: registerModule will not perform inclusions for you! + */ + public function registerModule($module, $overload = false) + { + if (is_string($module)) { + // attempt to load the module + $original_module = $module; + $ok = false; + foreach ($this->prefixes as $prefix) { + $module = $prefix . $original_module; + if (class_exists($module)) { + $ok = true; + break; + } + } + if (!$ok) { + $module = $original_module; + if (!class_exists($module)) { + trigger_error( + $original_module . ' module does not exist', + E_USER_ERROR + ); + return; + } + } + $module = new $module(); + } + if (empty($module->name)) { + trigger_error('Module instance of ' . get_class($module) . ' must have name'); + return; + } + if (!$overload && isset($this->registeredModules[$module->name])) { + trigger_error('Overloading ' . $module->name . ' without explicit overload parameter', E_USER_WARNING); + } + $this->registeredModules[$module->name] = $module; + } + + /** + * Adds a module to the current doctype by first registering it, + * and then tacking it on to the active doctype + */ + public function addModule($module) + { + $this->registerModule($module); + if (is_object($module)) { + $module = $module->name; + } + $this->userModules[] = $module; + } + + /** + * Adds a class prefix that registerModule() will use to resolve a + * string name to a concrete class + */ + public function addPrefix($prefix) + { + $this->prefixes[] = $prefix; + } + + /** + * Performs processing on modules, after being called you may + * use getElement() and getElements() + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + $this->trusted = $config->get('HTML.Trusted'); + + // generate + $this->doctype = $this->doctypes->make($config); + $modules = $this->doctype->modules; + + // take out the default modules that aren't allowed + $lookup = $config->get('HTML.AllowedModules'); + $special_cases = $config->get('HTML.CoreModules'); + + if (is_array($lookup)) { + foreach ($modules as $k => $m) { + if (isset($special_cases[$m])) { + continue; + } + if (!isset($lookup[$m])) { + unset($modules[$k]); + } + } + } + + // custom modules + if ($config->get('HTML.Proprietary')) { + $modules[] = 'Proprietary'; + } + if ($config->get('HTML.SafeObject')) { + $modules[] = 'SafeObject'; + } + if ($config->get('HTML.SafeEmbed')) { + $modules[] = 'SafeEmbed'; + } + if ($config->get('HTML.SafeScripting') !== array()) { + $modules[] = 'SafeScripting'; + } + if ($config->get('HTML.Nofollow')) { + $modules[] = 'Nofollow'; + } + if ($config->get('HTML.TargetBlank')) { + $modules[] = 'TargetBlank'; + } + // NB: HTML.TargetNoreferrer and HTML.TargetNoopener must be AFTER HTML.TargetBlank + // so that its post-attr-transform gets run afterwards. + if ($config->get('HTML.TargetNoreferrer')) { + $modules[] = 'TargetNoreferrer'; + } + if ($config->get('HTML.TargetNoopener')) { + $modules[] = 'TargetNoopener'; + } + + // merge in custom modules + $modules = array_merge($modules, $this->userModules); + + foreach ($modules as $module) { + $this->processModule($module); + $this->modules[$module]->setup($config); + } + + foreach ($this->doctype->tidyModules as $module) { + $this->processModule($module); + $this->modules[$module]->setup($config); + } + + // prepare any injectors + foreach ($this->modules as $module) { + $n = array(); + foreach ($module->info_injector as $injector) { + if (!is_object($injector)) { + $class = "HTMLPurifier_Injector_$injector"; + $injector = new $class; + } + $n[$injector->name] = $injector; + } + $module->info_injector = $n; + } + + // setup lookup table based on all valid modules + foreach ($this->modules as $module) { + foreach ($module->info as $name => $def) { + if (!isset($this->elementLookup[$name])) { + $this->elementLookup[$name] = array(); + } + $this->elementLookup[$name][] = $module->name; + } + } + + // note the different choice + $this->contentSets = new HTMLPurifier_ContentSets( + // content set assembly deals with all possible modules, + // not just ones deemed to be "safe" + $this->modules + ); + $this->attrCollections = new HTMLPurifier_AttrCollections( + $this->attrTypes, + // there is no way to directly disable a global attribute, + // but using AllowedAttributes or simply not including + // the module in your custom doctype should be sufficient + $this->modules + ); + } + + /** + * Takes a module and adds it to the active module collection, + * registering it if necessary. + */ + public function processModule($module) + { + if (!isset($this->registeredModules[$module]) || is_object($module)) { + $this->registerModule($module); + } + $this->modules[$module] = $this->registeredModules[$module]; + } + + /** + * Retrieves merged element definitions. + * @return Array of HTMLPurifier_ElementDef + */ + public function getElements() + { + $elements = array(); + foreach ($this->modules as $module) { + if (!$this->trusted && !$module->safe) { + continue; + } + foreach ($module->info as $name => $v) { + if (isset($elements[$name])) { + continue; + } + $elements[$name] = $this->getElement($name); + } + } + + // remove dud elements, this happens when an element that + // appeared to be safe actually wasn't + foreach ($elements as $n => $v) { + if ($v === false) { + unset($elements[$n]); + } + } + + return $elements; + + } + + /** + * Retrieves a single merged element definition + * @param string $name Name of element + * @param bool $trusted Boolean trusted overriding parameter: set to true + * if you want the full version of an element + * @return HTMLPurifier_ElementDef Merged HTMLPurifier_ElementDef + * @note You may notice that modules are getting iterated over twice (once + * in getElements() and once here). This + * is because + */ + public function getElement($name, $trusted = null) + { + if (!isset($this->elementLookup[$name])) { + return false; + } + + // setup global state variables + $def = false; + if ($trusted === null) { + $trusted = $this->trusted; + } + + // iterate through each module that has registered itself to this + // element + foreach ($this->elementLookup[$name] as $module_name) { + $module = $this->modules[$module_name]; + + // refuse to create/merge from a module that is deemed unsafe-- + // pretend the module doesn't exist--when trusted mode is not on. + if (!$trusted && !$module->safe) { + continue; + } + + // clone is used because, ideally speaking, the original + // definition should not be modified. Usually, this will + // make no difference, but for consistency's sake + $new_def = clone $module->info[$name]; + + if (!$def && $new_def->standalone) { + $def = $new_def; + } elseif ($def) { + // This will occur even if $new_def is standalone. In practice, + // this will usually result in a full replacement. + $def->mergeIn($new_def); + } else { + // :TODO: + // non-standalone definitions that don't have a standalone + // to merge into could be deferred to the end + // HOWEVER, it is perfectly valid for a non-standalone + // definition to lack a standalone definition, even + // after all processing: this allows us to safely + // specify extra attributes for elements that may not be + // enabled all in one place. In particular, this might + // be the case for trusted elements. WARNING: care must + // be taken that the /extra/ definitions are all safe. + continue; + } + + // attribute value expansions + $this->attrCollections->performInclusions($def->attr); + $this->attrCollections->expandIdentifiers($def->attr, $this->attrTypes); + + // descendants_are_inline, for ChildDef_Chameleon + if (is_string($def->content_model) && + strpos($def->content_model, 'Inline') !== false) { + if ($name != 'del' && $name != 'ins') { + // this is for you, ins/del + $def->descendants_are_inline = true; + } + } + + $this->contentSets->generateChildDef($def, $module); + } + + // This can occur if there is a blank definition, but no base to + // mix it in with + if (!$def) { + return false; + } + + // add information on required attributes + foreach ($def->attr as $attr_name => $attr_def) { + if ($attr_def->required) { + $def->required_attr[] = $attr_name; + } + } + return $def; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/IDAccumulator.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/IDAccumulator.php new file mode 100644 index 000000000..65c902c07 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/IDAccumulator.php @@ -0,0 +1,57 @@ +<?php + +/** + * Component of HTMLPurifier_AttrContext that accumulates IDs to prevent dupes + * @note In Slashdot-speak, dupe means duplicate. + * @note The default constructor does not accept $config or $context objects: + * use must use the static build() factory method to perform initialization. + */ +class HTMLPurifier_IDAccumulator +{ + + /** + * Lookup table of IDs we've accumulated. + * @public + */ + public $ids = array(); + + /** + * Builds an IDAccumulator, also initializing the default blacklist + * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config + * @param HTMLPurifier_Context $context Instance of HTMLPurifier_Context + * @return HTMLPurifier_IDAccumulator Fully initialized HTMLPurifier_IDAccumulator + */ + public static function build($config, $context) + { + $id_accumulator = new HTMLPurifier_IDAccumulator(); + $id_accumulator->load($config->get('Attr.IDBlacklist')); + return $id_accumulator; + } + + /** + * Add an ID to the lookup table. + * @param string $id ID to be added. + * @return bool status, true if success, false if there's a dupe + */ + public function add($id) + { + if (isset($this->ids[$id])) { + return false; + } + return $this->ids[$id] = true; + } + + /** + * Load a list of IDs into the lookup table + * @param $array_of_ids Array of IDs to load + * @note This function doesn't care about duplicates + */ + public function load($array_of_ids) + { + foreach ($array_of_ids as $id) { + $this->ids[$id] = true; + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector.php new file mode 100644 index 000000000..5060eef9e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector.php @@ -0,0 +1,281 @@ +<?php + +/** + * Injects tokens into the document while parsing for well-formedness. + * This enables "formatter-like" functionality such as auto-paragraphing, + * smiley-ification and linkification to take place. + * + * A note on how handlers create changes; this is done by assigning a new + * value to the $token reference. These values can take a variety of forms and + * are best described HTMLPurifier_Strategy_MakeWellFormed->processToken() + * documentation. + * + * @todo Allow injectors to request a re-run on their output. This + * would help if an operation is recursive. + */ +abstract class HTMLPurifier_Injector +{ + + /** + * Advisory name of injector, this is for friendly error messages. + * @type string + */ + public $name; + + /** + * @type HTMLPurifier_HTMLDefinition + */ + protected $htmlDefinition; + + /** + * Reference to CurrentNesting variable in Context. This is an array + * list of tokens that we are currently "inside" + * @type array + */ + protected $currentNesting; + + /** + * Reference to current token. + * @type HTMLPurifier_Token + */ + protected $currentToken; + + /** + * Reference to InputZipper variable in Context. + * @type HTMLPurifier_Zipper + */ + protected $inputZipper; + + /** + * Array of elements and attributes this injector creates and therefore + * need to be allowed by the definition. Takes form of + * array('element' => array('attr', 'attr2'), 'element2') + * @type array + */ + public $needed = array(); + + /** + * Number of elements to rewind backwards (relative). + * @type bool|int + */ + protected $rewindOffset = false; + + /** + * Rewind to a spot to re-perform processing. This is useful if you + * deleted a node, and now need to see if this change affected any + * earlier nodes. Rewinding does not affect other injectors, and can + * result in infinite loops if not used carefully. + * @param bool|int $offset + * @warning HTML Purifier will prevent you from fast-forwarding with this + * function. + */ + public function rewindOffset($offset) + { + $this->rewindOffset = $offset; + } + + /** + * Retrieves rewind offset, and then unsets it. + * @return bool|int + */ + public function getRewindOffset() + { + $r = $this->rewindOffset; + $this->rewindOffset = false; + return $r; + } + + /** + * Prepares the injector by giving it the config and context objects: + * this allows references to important variables to be made within + * the injector. This function also checks if the HTML environment + * will work with the Injector (see checkNeeded()). + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string Boolean false if success, string of missing needed element/attribute if failure + */ + public function prepare($config, $context) + { + $this->htmlDefinition = $config->getHTMLDefinition(); + // Even though this might fail, some unit tests ignore this and + // still test checkNeeded, so be careful. Maybe get rid of that + // dependency. + $result = $this->checkNeeded($config); + if ($result !== false) { + return $result; + } + $this->currentNesting =& $context->get('CurrentNesting'); + $this->currentToken =& $context->get('CurrentToken'); + $this->inputZipper =& $context->get('InputZipper'); + return false; + } + + /** + * This function checks if the HTML environment + * will work with the Injector: if p tags are not allowed, the + * Auto-Paragraphing injector should not be enabled. + * @param HTMLPurifier_Config $config + * @return bool|string Boolean false if success, string of missing needed element/attribute if failure + */ + public function checkNeeded($config) + { + $def = $config->getHTMLDefinition(); + foreach ($this->needed as $element => $attributes) { + if (is_int($element)) { + $element = $attributes; + } + if (!isset($def->info[$element])) { + return $element; + } + if (!is_array($attributes)) { + continue; + } + foreach ($attributes as $name) { + if (!isset($def->info[$element]->attr[$name])) { + return "$element.$name"; + } + } + } + return false; + } + + /** + * Tests if the context node allows a certain element + * @param string $name Name of element to test for + * @return bool True if element is allowed, false if it is not + */ + public function allowsElement($name) + { + if (!empty($this->currentNesting)) { + $parent_token = array_pop($this->currentNesting); + $this->currentNesting[] = $parent_token; + $parent = $this->htmlDefinition->info[$parent_token->name]; + } else { + $parent = $this->htmlDefinition->info_parent_def; + } + if (!isset($parent->child->elements[$name]) || isset($parent->excludes[$name])) { + return false; + } + // check for exclusion + for ($i = count($this->currentNesting) - 2; $i >= 0; $i--) { + $node = $this->currentNesting[$i]; + $def = $this->htmlDefinition->info[$node->name]; + if (isset($def->excludes[$name])) { + return false; + } + } + return true; + } + + /** + * Iterator function, which starts with the next token and continues until + * you reach the end of the input tokens. + * @warning Please prevent previous references from interfering with this + * functions by setting $i = null beforehand! + * @param int $i Current integer index variable for inputTokens + * @param HTMLPurifier_Token $current Current token variable. + * Do NOT use $token, as that variable is also a reference + * @return bool + */ + protected function forward(&$i, &$current) + { + if ($i === null) { + $i = count($this->inputZipper->back) - 1; + } else { + $i--; + } + if ($i < 0) { + return false; + } + $current = $this->inputZipper->back[$i]; + return true; + } + + /** + * Similar to _forward, but accepts a third parameter $nesting (which + * should be initialized at 0) and stops when we hit the end tag + * for the node $this->inputIndex starts in. + * @param int $i Current integer index variable for inputTokens + * @param HTMLPurifier_Token $current Current token variable. + * Do NOT use $token, as that variable is also a reference + * @param int $nesting + * @return bool + */ + protected function forwardUntilEndToken(&$i, &$current, &$nesting) + { + $result = $this->forward($i, $current); + if (!$result) { + return false; + } + if ($nesting === null) { + $nesting = 0; + } + if ($current instanceof HTMLPurifier_Token_Start) { + $nesting++; + } elseif ($current instanceof HTMLPurifier_Token_End) { + if ($nesting <= 0) { + return false; + } + $nesting--; + } + return true; + } + + /** + * Iterator function, starts with the previous token and continues until + * you reach the beginning of input tokens. + * @warning Please prevent previous references from interfering with this + * functions by setting $i = null beforehand! + * @param int $i Current integer index variable for inputTokens + * @param HTMLPurifier_Token $current Current token variable. + * Do NOT use $token, as that variable is also a reference + * @return bool + */ + protected function backward(&$i, &$current) + { + if ($i === null) { + $i = count($this->inputZipper->front) - 1; + } else { + $i--; + } + if ($i < 0) { + return false; + } + $current = $this->inputZipper->front[$i]; + return true; + } + + /** + * Handler that is called when a text token is processed + */ + public function handleText(&$token) + { + } + + /** + * Handler that is called when a start or empty token is processed + */ + public function handleElement(&$token) + { + } + + /** + * Handler that is called when an end token is processed + */ + public function handleEnd(&$token) + { + $this->notifyEnd($token); + } + + /** + * Notifier that is called when an end token is processed + * @param HTMLPurifier_Token $token Current token variable. + * @note This differs from handlers in that the token is read-only + * @deprecated + */ + public function notifyEnd($token) + { + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/AutoParagraph.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/AutoParagraph.php new file mode 100644 index 000000000..4afdd128d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/AutoParagraph.php @@ -0,0 +1,356 @@ +<?php + +/** + * Injector that auto paragraphs text in the root node based on + * double-spacing. + * @todo Ensure all states are unit tested, including variations as well. + * @todo Make a graph of the flow control for this Injector. + */ +class HTMLPurifier_Injector_AutoParagraph extends HTMLPurifier_Injector +{ + /** + * @type string + */ + public $name = 'AutoParagraph'; + + /** + * @type array + */ + public $needed = array('p'); + + /** + * @return HTMLPurifier_Token_Start + */ + private function _pStart() + { + $par = new HTMLPurifier_Token_Start('p'); + $par->armor['MakeWellFormed_TagClosedError'] = true; + return $par; + } + + /** + * @param HTMLPurifier_Token_Text $token + */ + public function handleText(&$token) + { + $text = $token->data; + // Does the current parent allow <p> tags? + if ($this->allowsElement('p')) { + if (empty($this->currentNesting) || strpos($text, "\n\n") !== false) { + // Note that we have differing behavior when dealing with text + // in the anonymous root node, or a node inside the document. + // If the text as a double-newline, the treatment is the same; + // if it doesn't, see the next if-block if you're in the document. + + $i = $nesting = null; + if (!$this->forwardUntilEndToken($i, $current, $nesting) && $token->is_whitespace) { + // State 1.1: ... ^ (whitespace, then document end) + // ---- + // This is a degenerate case + } else { + if (!$token->is_whitespace || $this->_isInline($current)) { + // State 1.2: PAR1 + // ---- + + // State 1.3: PAR1\n\nPAR2 + // ------------ + + // State 1.4: <div>PAR1\n\nPAR2 (see State 2) + // ------------ + $token = array($this->_pStart()); + $this->_splitText($text, $token); + } else { + // State 1.5: \n<hr /> + // -- + } + } + } else { + // State 2: <div>PAR1... (similar to 1.4) + // ---- + + // We're in an element that allows paragraph tags, but we're not + // sure if we're going to need them. + if ($this->_pLookAhead()) { + // State 2.1: <div>PAR1<b>PAR1\n\nPAR2 + // ---- + // Note: This will always be the first child, since any + // previous inline element would have triggered this very + // same routine, and found the double newline. One possible + // exception would be a comment. + $token = array($this->_pStart(), $token); + } else { + // State 2.2.1: <div>PAR1<div> + // ---- + + // State 2.2.2: <div>PAR1<b>PAR1</b></div> + // ---- + } + } + // Is the current parent a <p> tag? + } elseif (!empty($this->currentNesting) && + $this->currentNesting[count($this->currentNesting) - 1]->name == 'p') { + // State 3.1: ...<p>PAR1 + // ---- + + // State 3.2: ...<p>PAR1\n\nPAR2 + // ------------ + $token = array(); + $this->_splitText($text, $token); + // Abort! + } else { + // State 4.1: ...<b>PAR1 + // ---- + + // State 4.2: ...<b>PAR1\n\nPAR2 + // ------------ + } + } + + /** + * @param HTMLPurifier_Token $token + */ + public function handleElement(&$token) + { + // We don't have to check if we're already in a <p> tag for block + // tokens, because the tag would have been autoclosed by MakeWellFormed. + if ($this->allowsElement('p')) { + if (!empty($this->currentNesting)) { + if ($this->_isInline($token)) { + // State 1: <div>...<b> + // --- + // Check if this token is adjacent to the parent token + // (seek backwards until token isn't whitespace) + $i = null; + $this->backward($i, $prev); + + if (!$prev instanceof HTMLPurifier_Token_Start) { + // Token wasn't adjacent + if ($prev instanceof HTMLPurifier_Token_Text && + substr($prev->data, -2) === "\n\n" + ) { + // State 1.1.4: <div><p>PAR1</p>\n\n<b> + // --- + // Quite frankly, this should be handled by splitText + $token = array($this->_pStart(), $token); + } else { + // State 1.1.1: <div><p>PAR1</p><b> + // --- + // State 1.1.2: <div><br /><b> + // --- + // State 1.1.3: <div>PAR<b> + // --- + } + } else { + // State 1.2.1: <div><b> + // --- + // Lookahead to see if <p> is needed. + if ($this->_pLookAhead()) { + // State 1.3.1: <div><b>PAR1\n\nPAR2 + // --- + $token = array($this->_pStart(), $token); + } else { + // State 1.3.2: <div><b>PAR1</b></div> + // --- + + // State 1.3.3: <div><b>PAR1</b><div></div>\n\n</div> + // --- + } + } + } else { + // State 2.3: ...<div> + // ----- + } + } else { + if ($this->_isInline($token)) { + // State 3.1: <b> + // --- + // This is where the {p} tag is inserted, not reflected in + // inputTokens yet, however. + $token = array($this->_pStart(), $token); + } else { + // State 3.2: <div> + // ----- + } + + $i = null; + if ($this->backward($i, $prev)) { + if (!$prev instanceof HTMLPurifier_Token_Text) { + // State 3.1.1: ...</p>{p}<b> + // --- + // State 3.2.1: ...</p><div> + // ----- + if (!is_array($token)) { + $token = array($token); + } + array_unshift($token, new HTMLPurifier_Token_Text("\n\n")); + } else { + // State 3.1.2: ...</p>\n\n{p}<b> + // --- + // State 3.2.2: ...</p>\n\n<div> + // ----- + // Note: PAR<ELEM> cannot occur because PAR would have been + // wrapped in <p> tags. + } + } + } + } else { + // State 2.2: <ul><li> + // ---- + // State 2.4: <p><b> + // --- + } + } + + /** + * Splits up a text in paragraph tokens and appends them + * to the result stream that will replace the original + * @param string $data String text data that will be processed + * into paragraphs + * @param HTMLPurifier_Token[] $result Reference to array of tokens that the + * tags will be appended onto + */ + private function _splitText($data, &$result) + { + $raw_paragraphs = explode("\n\n", $data); + $paragraphs = array(); // without empty paragraphs + $needs_start = false; + $needs_end = false; + + $c = count($raw_paragraphs); + if ($c == 1) { + // There were no double-newlines, abort quickly. In theory this + // should never happen. + $result[] = new HTMLPurifier_Token_Text($data); + return; + } + for ($i = 0; $i < $c; $i++) { + $par = $raw_paragraphs[$i]; + if (trim($par) !== '') { + $paragraphs[] = $par; + } else { + if ($i == 0) { + // Double newline at the front + if (empty($result)) { + // The empty result indicates that the AutoParagraph + // injector did not add any start paragraph tokens. + // This means that we have been in a paragraph for + // a while, and the newline means we should start a new one. + $result[] = new HTMLPurifier_Token_End('p'); + $result[] = new HTMLPurifier_Token_Text("\n\n"); + // However, the start token should only be added if + // there is more processing to be done (i.e. there are + // real paragraphs in here). If there are none, the + // next start paragraph tag will be handled by the + // next call to the injector + $needs_start = true; + } else { + // We just started a new paragraph! + // Reinstate a double-newline for presentation's sake, since + // it was in the source code. + array_unshift($result, new HTMLPurifier_Token_Text("\n\n")); + } + } elseif ($i + 1 == $c) { + // Double newline at the end + // There should be a trailing </p> when we're finally done. + $needs_end = true; + } + } + } + + // Check if this was just a giant blob of whitespace. Move this earlier, + // perhaps? + if (empty($paragraphs)) { + return; + } + + // Add the start tag indicated by \n\n at the beginning of $data + if ($needs_start) { + $result[] = $this->_pStart(); + } + + // Append the paragraphs onto the result + foreach ($paragraphs as $par) { + $result[] = new HTMLPurifier_Token_Text($par); + $result[] = new HTMLPurifier_Token_End('p'); + $result[] = new HTMLPurifier_Token_Text("\n\n"); + $result[] = $this->_pStart(); + } + + // Remove trailing start token; Injector will handle this later if + // it was indeed needed. This prevents from needing to do a lookahead, + // at the cost of a lookbehind later. + array_pop($result); + + // If there is no need for an end tag, remove all of it and let + // MakeWellFormed close it later. + if (!$needs_end) { + array_pop($result); // removes \n\n + array_pop($result); // removes </p> + } + } + + /** + * Returns true if passed token is inline (and, ergo, allowed in + * paragraph tags) + * @param HTMLPurifier_Token $token + * @return bool + */ + private function _isInline($token) + { + return isset($this->htmlDefinition->info['p']->child->elements[$token->name]); + } + + /** + * Looks ahead in the token list and determines whether or not we need + * to insert a <p> tag. + * @return bool + */ + private function _pLookAhead() + { + if ($this->currentToken instanceof HTMLPurifier_Token_Start) { + $nesting = 1; + } else { + $nesting = 0; + } + $ok = false; + $i = null; + while ($this->forwardUntilEndToken($i, $current, $nesting)) { + $result = $this->_checkNeedsP($current); + if ($result !== null) { + $ok = $result; + break; + } + } + return $ok; + } + + /** + * Determines if a particular token requires an earlier inline token + * to get a paragraph. This should be used with _forwardUntilEndToken + * @param HTMLPurifier_Token $current + * @return bool + */ + private function _checkNeedsP($current) + { + if ($current instanceof HTMLPurifier_Token_Start) { + if (!$this->_isInline($current)) { + // <div>PAR1<div> + // ---- + // Terminate early, since we hit a block element + return false; + } + } elseif ($current instanceof HTMLPurifier_Token_Text) { + if (strpos($current->data, "\n\n") !== false) { + // <div>PAR1<b>PAR1\n\nPAR2 + // ---- + return true; + } else { + // <div>PAR1<b>PAR1... + // ---- + } + } + return null; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/DisplayLinkURI.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/DisplayLinkURI.php new file mode 100644 index 000000000..c19b1bc27 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/DisplayLinkURI.php @@ -0,0 +1,40 @@ +<?php + +/** + * Injector that displays the URL of an anchor instead of linking to it, in addition to showing the text of the link. + */ +class HTMLPurifier_Injector_DisplayLinkURI extends HTMLPurifier_Injector +{ + /** + * @type string + */ + public $name = 'DisplayLinkURI'; + + /** + * @type array + */ + public $needed = array('a'); + + /** + * @param $token + */ + public function handleElement(&$token) + { + } + + /** + * @param HTMLPurifier_Token $token + */ + public function handleEnd(&$token) + { + if (isset($token->start->attr['href'])) { + $url = $token->start->attr['href']; + unset($token->start->attr['href']); + $token = array($token, new HTMLPurifier_Token_Text(" ($url)")); + } else { + // nothing to display + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/Linkify.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/Linkify.php new file mode 100644 index 000000000..74f83eaa7 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/Linkify.php @@ -0,0 +1,64 @@ +<?php + +/** + * Injector that converts http, https and ftp text URLs to actual links. + */ +class HTMLPurifier_Injector_Linkify extends HTMLPurifier_Injector +{ + /** + * @type string + */ + public $name = 'Linkify'; + + /** + * @type array + */ + public $needed = array('a' => array('href')); + + /** + * @param HTMLPurifier_Token $token + */ + public function handleText(&$token) + { + if (!$this->allowsElement('a')) { + return; + } + + if (strpos($token->data, '://') === false) { + // our really quick heuristic failed, abort + // this may not work so well if we want to match things like + // "google.com", but then again, most people don't + return; + } + + // there is/are URL(s). Let's split the string. + // We use this regex: + // https://gist.github.com/gruber/249502 + // but with @cscott's backtracking fix and also + // the Unicode characters un-Unicodified. + $bits = preg_split( + '/\\b((?:[a-z][\\w\\-]+:(?:\\/{1,3}|[a-z0-9%])|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,4}\\/)(?:[^\\s()<>]|\\((?:[^\\s()<>]|(?:\\([^\\s()<>]+\\)))*\\))+(?:\\((?:[^\\s()<>]|(?:\\([^\\s()<>]+\\)))*\\)|[^\\s`!()\\[\\]{};:\'".,<>?\x{00ab}\x{00bb}\x{201c}\x{201d}\x{2018}\x{2019}]))/iu', + $token->data, -1, PREG_SPLIT_DELIM_CAPTURE); + + + $token = array(); + + // $i = index + // $c = count + // $l = is link + for ($i = 0, $c = count($bits), $l = false; $i < $c; $i++, $l = !$l) { + if (!$l) { + if ($bits[$i] === '') { + continue; + } + $token[] = new HTMLPurifier_Token_Text($bits[$i]); + } else { + $token[] = new HTMLPurifier_Token_Start('a', array('href' => $bits[$i])); + $token[] = new HTMLPurifier_Token_Text($bits[$i]); + $token[] = new HTMLPurifier_Token_End('a'); + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/PurifierLinkify.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/PurifierLinkify.php new file mode 100644 index 000000000..cb9046f33 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/PurifierLinkify.php @@ -0,0 +1,71 @@ +<?php + +/** + * Injector that converts configuration directive syntax %Namespace.Directive + * to links + */ +class HTMLPurifier_Injector_PurifierLinkify extends HTMLPurifier_Injector +{ + /** + * @type string + */ + public $name = 'PurifierLinkify'; + + /** + * @type string + */ + public $docURL; + + /** + * @type array + */ + public $needed = array('a' => array('href')); + + /** + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function prepare($config, $context) + { + $this->docURL = $config->get('AutoFormat.PurifierLinkify.DocURL'); + return parent::prepare($config, $context); + } + + /** + * @param HTMLPurifier_Token $token + */ + public function handleText(&$token) + { + if (!$this->allowsElement('a')) { + return; + } + if (strpos($token->data, '%') === false) { + return; + } + + $bits = preg_split('#%([a-z0-9]+\.[a-z0-9]+)#Si', $token->data, -1, PREG_SPLIT_DELIM_CAPTURE); + $token = array(); + + // $i = index + // $c = count + // $l = is link + for ($i = 0, $c = count($bits), $l = false; $i < $c; $i++, $l = !$l) { + if (!$l) { + if ($bits[$i] === '') { + continue; + } + $token[] = new HTMLPurifier_Token_Text($bits[$i]); + } else { + $token[] = new HTMLPurifier_Token_Start( + 'a', + array('href' => str_replace('%s', $bits[$i], $this->docURL)) + ); + $token[] = new HTMLPurifier_Token_Text('%' . $bits[$i]); + $token[] = new HTMLPurifier_Token_End('a'); + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/RemoveEmpty.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/RemoveEmpty.php new file mode 100644 index 000000000..0ebc477c6 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/RemoveEmpty.php @@ -0,0 +1,112 @@ +<?php + +class HTMLPurifier_Injector_RemoveEmpty extends HTMLPurifier_Injector +{ + /** + * @type HTMLPurifier_Context + */ + private $context; + + /** + * @type HTMLPurifier_Config + */ + private $config; + + /** + * @type HTMLPurifier_AttrValidator + */ + private $attrValidator; + + /** + * @type bool + */ + private $removeNbsp; + + /** + * @type bool + */ + private $removeNbspExceptions; + + /** + * Cached contents of %AutoFormat.RemoveEmpty.Predicate + * @type array + */ + private $exclude; + + /** + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return void + */ + public function prepare($config, $context) + { + parent::prepare($config, $context); + $this->config = $config; + $this->context = $context; + $this->removeNbsp = $config->get('AutoFormat.RemoveEmpty.RemoveNbsp'); + $this->removeNbspExceptions = $config->get('AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions'); + $this->exclude = $config->get('AutoFormat.RemoveEmpty.Predicate'); + foreach ($this->exclude as $key => $attrs) { + if (!is_array($attrs)) { + // HACK, see HTMLPurifier/Printer/ConfigForm.php + $this->exclude[$key] = explode(';', $attrs); + } + } + $this->attrValidator = new HTMLPurifier_AttrValidator(); + } + + /** + * @param HTMLPurifier_Token $token + */ + public function handleElement(&$token) + { + if (!$token instanceof HTMLPurifier_Token_Start) { + return; + } + $next = false; + $deleted = 1; // the current tag + for ($i = count($this->inputZipper->back) - 1; $i >= 0; $i--, $deleted++) { + $next = $this->inputZipper->back[$i]; + if ($next instanceof HTMLPurifier_Token_Text) { + if ($next->is_whitespace) { + continue; + } + if ($this->removeNbsp && !isset($this->removeNbspExceptions[$token->name])) { + $plain = str_replace("\xC2\xA0", "", $next->data); + $isWsOrNbsp = $plain === '' || ctype_space($plain); + if ($isWsOrNbsp) { + continue; + } + } + } + break; + } + if (!$next || ($next instanceof HTMLPurifier_Token_End && $next->name == $token->name)) { + $this->attrValidator->validateToken($token, $this->config, $this->context); + $token->armor['ValidateAttributes'] = true; + if (isset($this->exclude[$token->name])) { + $r = true; + foreach ($this->exclude[$token->name] as $elem) { + if (!isset($token->attr[$elem])) $r = false; + } + if ($r) return; + } + if (isset($token->attr['id']) || isset($token->attr['name'])) { + return; + } + $token = $deleted + 1; + for ($b = 0, $c = count($this->inputZipper->front); $b < $c; $b++) { + $prev = $this->inputZipper->front[$b]; + if ($prev instanceof HTMLPurifier_Token_Text && $prev->is_whitespace) { + continue; + } + break; + } + // This is safe because we removed the token that triggered this. + $this->rewindOffset($b+$deleted); + return; + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/RemoveSpansWithoutAttributes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/RemoveSpansWithoutAttributes.php new file mode 100644 index 000000000..9ee7aa84d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/RemoveSpansWithoutAttributes.php @@ -0,0 +1,84 @@ +<?php + +/** + * Injector that removes spans with no attributes + */ +class HTMLPurifier_Injector_RemoveSpansWithoutAttributes extends HTMLPurifier_Injector +{ + /** + * @type string + */ + public $name = 'RemoveSpansWithoutAttributes'; + + /** + * @type array + */ + public $needed = array('span'); + + /** + * @type HTMLPurifier_AttrValidator + */ + private $attrValidator; + + /** + * Used by AttrValidator. + * @type HTMLPurifier_Config + */ + private $config; + + /** + * @type HTMLPurifier_Context + */ + private $context; + + public function prepare($config, $context) + { + $this->attrValidator = new HTMLPurifier_AttrValidator(); + $this->config = $config; + $this->context = $context; + return parent::prepare($config, $context); + } + + /** + * @param HTMLPurifier_Token $token + */ + public function handleElement(&$token) + { + if ($token->name !== 'span' || !$token instanceof HTMLPurifier_Token_Start) { + return; + } + + // We need to validate the attributes now since this doesn't normally + // happen until after MakeWellFormed. If all the attributes are removed + // the span needs to be removed too. + $this->attrValidator->validateToken($token, $this->config, $this->context); + $token->armor['ValidateAttributes'] = true; + + if (!empty($token->attr)) { + return; + } + + $nesting = 0; + while ($this->forwardUntilEndToken($i, $current, $nesting)) { + } + + if ($current instanceof HTMLPurifier_Token_End && $current->name === 'span') { + // Mark closing span tag for deletion + $current->markForDeletion = true; + // Delete open span tag + $token = false; + } + } + + /** + * @param HTMLPurifier_Token $token + */ + public function handleEnd(&$token) + { + if ($token->markForDeletion) { + $token = false; + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/SafeObject.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/SafeObject.php new file mode 100644 index 000000000..317f7864d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/SafeObject.php @@ -0,0 +1,124 @@ +<?php + +/** + * Adds important param elements to inside of object in order to make + * things safe. + */ +class HTMLPurifier_Injector_SafeObject extends HTMLPurifier_Injector +{ + /** + * @type string + */ + public $name = 'SafeObject'; + + /** + * @type array + */ + public $needed = array('object', 'param'); + + /** + * @type array + */ + protected $objectStack = array(); + + /** + * @type array + */ + protected $paramStack = array(); + + /** + * Keep this synchronized with AttrTransform/SafeParam.php. + * @type array + */ + protected $addParam = array( + 'allowScriptAccess' => 'never', + 'allowNetworking' => 'internal', + ); + + /** + * These are all lower-case keys. + * @type array + */ + protected $allowedParam = array( + 'wmode' => true, + 'movie' => true, + 'flashvars' => true, + 'src' => true, + 'allowfullscreen' => true, // if omitted, assume to be 'false' + ); + + /** + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return void + */ + public function prepare($config, $context) + { + parent::prepare($config, $context); + } + + /** + * @param HTMLPurifier_Token $token + */ + public function handleElement(&$token) + { + if ($token->name == 'object') { + $this->objectStack[] = $token; + $this->paramStack[] = array(); + $new = array($token); + foreach ($this->addParam as $name => $value) { + $new[] = new HTMLPurifier_Token_Empty('param', array('name' => $name, 'value' => $value)); + } + $token = $new; + } elseif ($token->name == 'param') { + $nest = count($this->currentNesting) - 1; + if ($nest >= 0 && $this->currentNesting[$nest]->name === 'object') { + $i = count($this->objectStack) - 1; + if (!isset($token->attr['name'])) { + $token = false; + return; + } + $n = $token->attr['name']; + // We need this fix because YouTube doesn't supply a data + // attribute, which we need if a type is specified. This is + // *very* Flash specific. + if (!isset($this->objectStack[$i]->attr['data']) && + ($token->attr['name'] == 'movie' || $token->attr['name'] == 'src') + ) { + $this->objectStack[$i]->attr['data'] = $token->attr['value']; + } + // Check if the parameter is the correct value but has not + // already been added + if (!isset($this->paramStack[$i][$n]) && + isset($this->addParam[$n]) && + $token->attr['name'] === $this->addParam[$n]) { + // keep token, and add to param stack + $this->paramStack[$i][$n] = true; + } elseif (isset($this->allowedParam[strtolower($n)])) { + // keep token, don't do anything to it + // (could possibly check for duplicates here) + // Note: In principle, parameters should be case sensitive. + // But it seems they are not really; so accept any case. + } else { + $token = false; + } + } else { + // not directly inside an object, DENY! + $token = false; + } + } + } + + public function handleEnd(&$token) + { + // This is the WRONG way of handling the object and param stacks; + // we should be inserting them directly on the relevant object tokens + // so that the global stack handling handles it. + if ($token->name == 'object') { + array_pop($this->objectStack); + array_pop($this->paramStack); + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language.php new file mode 100644 index 000000000..65277dd43 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language.php @@ -0,0 +1,204 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language/classes/en-x-test.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language/classes/en-x-test.php new file mode 100644 index 000000000..8828f5cde --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language/classes/en-x-test.php @@ -0,0 +1,9 @@ +<?php + +// private class for unit testing + +class HTMLPurifier_Language_en_x_test extends HTMLPurifier_Language +{ +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language/messages/en-x-test.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language/messages/en-x-test.php new file mode 100644 index 000000000..1c046f379 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language/messages/en-x-test.php @@ -0,0 +1,11 @@ +<?php + +// private language message file for unit testing purposes + +$fallback = 'en'; + +$messages = array( + 'HTMLPurifier' => 'HTML Purifier X' +); + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language/messages/en-x-testmini.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language/messages/en-x-testmini.php new file mode 100644 index 000000000..806c83fbf --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language/messages/en-x-testmini.php @@ -0,0 +1,12 @@ +<?php + +// private language message file for unit testing purposes +// this language file has no class associated with it + +$fallback = 'en'; + +$messages = array( + 'HTMLPurifier' => 'HTML Purifier XNone' +); + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language/messages/en.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language/messages/en.php new file mode 100644 index 000000000..c7f197e1e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language/messages/en.php @@ -0,0 +1,55 @@ +<?php + +$fallback = false; + +$messages = array( + + 'HTMLPurifier' => 'HTML Purifier', +// for unit testing purposes + 'LanguageFactoryTest: Pizza' => 'Pizza', + 'LanguageTest: List' => '$1', + 'LanguageTest: Hash' => '$1.Keys; $1.Values', + 'Item separator' => ', ', + 'Item separator last' => ' and ', // non-Harvard style + + 'ErrorCollector: No errors' => 'No errors detected. However, because error reporting is still incomplete, there may have been errors that the error collector was not notified of; please inspect the output HTML carefully.', + 'ErrorCollector: At line' => ' at line $line', + 'ErrorCollector: Incidental errors' => 'Incidental errors', + 'Lexer: Unclosed comment' => 'Unclosed comment', + 'Lexer: Unescaped lt' => 'Unescaped less-than sign (<) should be <', + 'Lexer: Missing gt' => 'Missing greater-than sign (>), previous less-than sign (<) should be escaped', + 'Lexer: Missing attribute key' => 'Attribute declaration has no key', + 'Lexer: Missing end quote' => 'Attribute declaration has no end quote', + 'Lexer: Extracted body' => 'Removed document metadata tags', + 'Strategy_RemoveForeignElements: Tag transform' => '<$1> element transformed into $CurrentToken.Serialized', + 'Strategy_RemoveForeignElements: Missing required attribute' => '$CurrentToken.Compact element missing required attribute $1', + 'Strategy_RemoveForeignElements: Foreign element to text' => 'Unrecognized $CurrentToken.Serialized tag converted to text', + 'Strategy_RemoveForeignElements: Foreign element removed' => 'Unrecognized $CurrentToken.Serialized tag removed', + 'Strategy_RemoveForeignElements: Comment removed' => 'Comment containing "$CurrentToken.Data" removed', + 'Strategy_RemoveForeignElements: Foreign meta element removed' => 'Unrecognized $CurrentToken.Serialized meta tag and all descendants removed', + 'Strategy_RemoveForeignElements: Token removed to end' => 'Tags and text starting from $1 element where removed to end', + 'Strategy_RemoveForeignElements: Trailing hyphen in comment removed' => 'Trailing hyphen(s) in comment removed', + 'Strategy_RemoveForeignElements: Hyphens in comment collapsed' => 'Double hyphens in comments are not allowed, and were collapsed into single hyphens', + 'Strategy_MakeWellFormed: Unnecessary end tag removed' => 'Unnecessary $CurrentToken.Serialized tag removed', + 'Strategy_MakeWellFormed: Unnecessary end tag to text' => 'Unnecessary $CurrentToken.Serialized tag converted to text', + 'Strategy_MakeWellFormed: Tag auto closed' => '$1.Compact started on line $1.Line auto-closed by $CurrentToken.Compact', + 'Strategy_MakeWellFormed: Tag carryover' => '$1.Compact started on line $1.Line auto-continued into $CurrentToken.Compact', + 'Strategy_MakeWellFormed: Stray end tag removed' => 'Stray $CurrentToken.Serialized tag removed', + 'Strategy_MakeWellFormed: Stray end tag to text' => 'Stray $CurrentToken.Serialized tag converted to text', + 'Strategy_MakeWellFormed: Tag closed by element end' => '$1.Compact tag started on line $1.Line closed by end of $CurrentToken.Serialized', + 'Strategy_MakeWellFormed: Tag closed by document end' => '$1.Compact tag started on line $1.Line closed by end of document', + 'Strategy_FixNesting: Node removed' => '$CurrentToken.Compact node removed', + 'Strategy_FixNesting: Node excluded' => '$CurrentToken.Compact node removed due to descendant exclusion by ancestor element', + 'Strategy_FixNesting: Node reorganized' => 'Contents of $CurrentToken.Compact node reorganized to enforce its content model', + 'Strategy_FixNesting: Node contents removed' => 'Contents of $CurrentToken.Compact node removed', + 'AttrValidator: Attributes transformed' => 'Attributes on $CurrentToken.Compact transformed from $1.Keys to $2.Keys', + 'AttrValidator: Attribute removed' => '$CurrentAttr.Name attribute on $CurrentToken.Compact removed', +); + +$errorNames = array( + E_ERROR => 'Error', + E_WARNING => 'Warning', + E_NOTICE => 'Notice' +); + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/LanguageFactory.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/LanguageFactory.php new file mode 100644 index 000000000..4e35272d8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/LanguageFactory.php @@ -0,0 +1,209 @@ +<?php + +/** + * Class responsible for generating HTMLPurifier_Language objects, managing + * caching and fallbacks. + * @note Thanks to MediaWiki for the general logic, although this version + * has been entirely rewritten + * @todo Serialized cache for languages + */ +class HTMLPurifier_LanguageFactory +{ + + /** + * Cache of language code information used to load HTMLPurifier_Language objects. + * Structure is: $factory->cache[$language_code][$key] = $value + * @type array + */ + public $cache; + + /** + * Valid keys in the HTMLPurifier_Language object. Designates which + * variables to slurp out of a message file. + * @type array + */ + public $keys = array('fallback', 'messages', 'errorNames'); + + /** + * Instance to validate language codes. + * @type HTMLPurifier_AttrDef_Lang + * + */ + protected $validator; + + /** + * Cached copy of dirname(__FILE__), directory of current file without + * trailing slash. + * @type string + */ + protected $dir; + + /** + * Keys whose contents are a hash map and can be merged. + * @type array + */ + protected $mergeable_keys_map = array('messages' => true, 'errorNames' => true); + + /** + * Keys whose contents are a list and can be merged. + * @value array lookup + */ + protected $mergeable_keys_list = array(); + + /** + * Retrieve sole instance of the factory. + * @param HTMLPurifier_LanguageFactory $prototype Optional prototype to overload sole instance with, + * or bool true to reset to default factory. + * @return HTMLPurifier_LanguageFactory + */ + public static function instance($prototype = null) + { + static $instance = null; + if ($prototype !== null) { + $instance = $prototype; + } elseif ($instance === null || $prototype == true) { + $instance = new HTMLPurifier_LanguageFactory(); + $instance->setup(); + } + return $instance; + } + + /** + * Sets up the singleton, much like a constructor + * @note Prevents people from getting this outside of the singleton + */ + public function setup() + { + $this->validator = new HTMLPurifier_AttrDef_Lang(); + $this->dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier'; + } + + /** + * Creates a language object, handles class fallbacks + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @param bool|string $code Code to override configuration with. Private parameter. + * @return HTMLPurifier_Language + */ + public function create($config, $context, $code = false) + { + // validate language code + if ($code === false) { + $code = $this->validator->validate( + $config->get('Core.Language'), + $config, + $context + ); + } else { + $code = $this->validator->validate($code, $config, $context); + } + if ($code === false) { + $code = 'en'; // malformed code becomes English + } + + $pcode = str_replace('-', '_', $code); // make valid PHP classname + static $depth = 0; // recursion protection + + if ($code == 'en') { + $lang = new HTMLPurifier_Language($config, $context); + } else { + $class = 'HTMLPurifier_Language_' . $pcode; + $file = $this->dir . '/Language/classes/' . $code . '.php'; + if (file_exists($file) || class_exists($class, false)) { + $lang = new $class($config, $context); + } else { + // Go fallback + $raw_fallback = $this->getFallbackFor($code); + $fallback = $raw_fallback ? $raw_fallback : 'en'; + $depth++; + $lang = $this->create($config, $context, $fallback); + if (!$raw_fallback) { + $lang->error = true; + } + $depth--; + } + } + $lang->code = $code; + return $lang; + } + + /** + * Returns the fallback language for language + * @note Loads the original language into cache + * @param string $code language code + * @return string|bool + */ + public function getFallbackFor($code) + { + $this->loadLanguage($code); + return $this->cache[$code]['fallback']; + } + + /** + * Loads language into the cache, handles message file and fallbacks + * @param string $code language code + */ + public function loadLanguage($code) + { + static $languages_seen = array(); // recursion guard + + // abort if we've already loaded it + if (isset($this->cache[$code])) { + return; + } + + // generate filename + $filename = $this->dir . '/Language/messages/' . $code . '.php'; + + // default fallback : may be overwritten by the ensuing include + $fallback = ($code != 'en') ? 'en' : false; + + // load primary localisation + if (!file_exists($filename)) { + // skip the include: will rely solely on fallback + $filename = $this->dir . '/Language/messages/en.php'; + $cache = array(); + } else { + include $filename; + $cache = compact($this->keys); + } + + // load fallback localisation + if (!empty($fallback)) { + + // infinite recursion guard + if (isset($languages_seen[$code])) { + trigger_error( + 'Circular fallback reference in language ' . + $code, + E_USER_ERROR + ); + $fallback = 'en'; + } + $language_seen[$code] = true; + + // load the fallback recursively + $this->loadLanguage($fallback); + $fallback_cache = $this->cache[$fallback]; + + // merge fallback with current language + foreach ($this->keys as $key) { + if (isset($cache[$key]) && isset($fallback_cache[$key])) { + if (isset($this->mergeable_keys_map[$key])) { + $cache[$key] = $cache[$key] + $fallback_cache[$key]; + } elseif (isset($this->mergeable_keys_list[$key])) { + $cache[$key] = array_merge($fallback_cache[$key], $cache[$key]); + } + } else { + $cache[$key] = $fallback_cache[$key]; + } + } + } + + // save to cache for later retrieval + $this->cache[$code] = $cache; + return; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Length.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Length.php new file mode 100644 index 000000000..bbfbe6624 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Length.php @@ -0,0 +1,160 @@ +<?php + +/** + * Represents a measurable length, with a string numeric magnitude + * and a unit. This object is immutable. + */ +class HTMLPurifier_Length +{ + + /** + * String numeric magnitude. + * @type string + */ + protected $n; + + /** + * String unit. False is permitted if $n = 0. + * @type string|bool + */ + protected $unit; + + /** + * Whether or not this length is valid. Null if not calculated yet. + * @type bool + */ + protected $isValid; + + /** + * Array Lookup array of units recognized by CSS 2.1 + * @type array + */ + protected static $allowedUnits = array( + 'em' => true, 'ex' => true, 'px' => true, 'in' => true, + 'cm' => true, 'mm' => true, 'pt' => true, 'pc' => true + ); + + /** + * @param string $n Magnitude + * @param bool|string $u Unit + */ + public function __construct($n = '0', $u = false) + { + $this->n = (string) $n; + $this->unit = $u !== false ? (string) $u : false; + } + + /** + * @param string $s Unit string, like '2em' or '3.4in' + * @return HTMLPurifier_Length + * @warning Does not perform validation. + */ + public static function make($s) + { + if ($s instanceof HTMLPurifier_Length) { + return $s; + } + $n_length = strspn($s, '1234567890.+-'); + $n = substr($s, 0, $n_length); + $unit = substr($s, $n_length); + if ($unit === '') { + $unit = false; + } + return new HTMLPurifier_Length($n, $unit); + } + + /** + * Validates the number and unit. + * @return bool + */ + protected function validate() + { + // Special case: + if ($this->n === '+0' || $this->n === '-0') { + $this->n = '0'; + } + if ($this->n === '0' && $this->unit === false) { + return true; + } + if (!ctype_lower($this->unit)) { + $this->unit = strtolower($this->unit); + } + if (!isset(HTMLPurifier_Length::$allowedUnits[$this->unit])) { + return false; + } + // Hack: + $def = new HTMLPurifier_AttrDef_CSS_Number(); + $result = $def->validate($this->n, false, false); + if ($result === false) { + return false; + } + $this->n = $result; + return true; + } + + /** + * Returns string representation of number. + * @return string + */ + public function toString() + { + if (!$this->isValid()) { + return false; + } + return $this->n . $this->unit; + } + + /** + * Retrieves string numeric magnitude. + * @return string + */ + public function getN() + { + return $this->n; + } + + /** + * Retrieves string unit. + * @return string + */ + public function getUnit() + { + return $this->unit; + } + + /** + * Returns true if this length unit is valid. + * @return bool + */ + public function isValid() + { + if ($this->isValid === null) { + $this->isValid = $this->validate(); + } + return $this->isValid; + } + + /** + * Compares two lengths, and returns 1 if greater, -1 if less and 0 if equal. + * @param HTMLPurifier_Length $l + * @return int + * @warning If both values are too large or small, this calculation will + * not work properly + */ + public function compareTo($l) + { + if ($l === false) { + return false; + } + if ($l->unit !== $this->unit) { + $converter = new HTMLPurifier_UnitConverter(); + $l = $converter->convert($l, $this->unit); + if ($l === false) { + return false; + } + } + return $this->n - $l->n; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer.php new file mode 100644 index 000000000..99b3c7df0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer.php @@ -0,0 +1,382 @@ +<?php + +/** + * Forgivingly lexes HTML (SGML-style) markup into tokens. + * + * A lexer parses a string of SGML-style markup and converts them into + * corresponding tokens. It doesn't check for well-formedness, although its + * internal mechanism may make this automatic (such as the case of + * HTMLPurifier_Lexer_DOMLex). There are several implementations to choose + * from. + * + * A lexer is HTML-oriented: it might work with XML, but it's not + * recommended, as we adhere to a subset of the specification for optimization + * reasons. This might change in the future. Also, most tokenizers are not + * expected to handle DTDs or PIs. + * + * This class should not be directly instantiated, but you may use create() to + * retrieve a default copy of the lexer. Being a supertype, this class + * does not actually define any implementation, but offers commonly used + * convenience functions for subclasses. + * + * @note The unit tests will instantiate this class for testing purposes, as + * many of the utility functions require a class to be instantiated. + * This means that, even though this class is not runnable, it will + * not be declared abstract. + * + * @par + * + * @note + * We use tokens rather than create a DOM representation because DOM would: + * + * @par + * -# Require more processing and memory to create, + * -# Is not streamable, and + * -# Has the entire document structure (html and body not needed). + * + * @par + * However, DOM is helpful in that it makes it easy to move around nodes + * without a lot of lookaheads to see when a tag is closed. This is a + * limitation of the token system and some workarounds would be nice. + */ +class HTMLPurifier_Lexer +{ + + /** + * Whether or not this lexer implements line-number/column-number tracking. + * If it does, set to true. + */ + public $tracksLineNumbers = false; + + // -- STATIC ---------------------------------------------------------- + + /** + * Retrieves or sets the default Lexer as a Prototype Factory. + * + * By default HTMLPurifier_Lexer_DOMLex will be returned. There are + * a few exceptions involving special features that only DirectLex + * implements. + * + * @note The behavior of this class has changed, rather than accepting + * a prototype object, it now accepts a configuration object. + * To specify your own prototype, set %Core.LexerImpl to it. + * This change in behavior de-singletonizes the lexer object. + * + * @param HTMLPurifier_Config $config + * @return HTMLPurifier_Lexer + * @throws HTMLPurifier_Exception + */ + public static function create($config) + { + if (!($config instanceof HTMLPurifier_Config)) { + $lexer = $config; + trigger_error( + "Passing a prototype to + HTMLPurifier_Lexer::create() is deprecated, please instead + use %Core.LexerImpl", + E_USER_WARNING + ); + } else { + $lexer = $config->get('Core.LexerImpl'); + } + + $needs_tracking = + $config->get('Core.MaintainLineNumbers') || + $config->get('Core.CollectErrors'); + + $inst = null; + if (is_object($lexer)) { + $inst = $lexer; + } else { + if (is_null($lexer)) { + do { + // auto-detection algorithm + if ($needs_tracking) { + $lexer = 'DirectLex'; + break; + } + + if (class_exists('DOMDocument') && + method_exists('DOMDocument', 'loadHTML') && + !extension_loaded('domxml') + ) { + // check for DOM support, because while it's part of the + // core, it can be disabled compile time. Also, the PECL + // domxml extension overrides the default DOM, and is evil + // and nasty and we shan't bother to support it + $lexer = 'DOMLex'; + } else { + $lexer = 'DirectLex'; + } + } while (0); + } // do..while so we can break + + // instantiate recognized string names + switch ($lexer) { + case 'DOMLex': + $inst = new HTMLPurifier_Lexer_DOMLex(); + break; + case 'DirectLex': + $inst = new HTMLPurifier_Lexer_DirectLex(); + break; + case 'PH5P': + $inst = new HTMLPurifier_Lexer_PH5P(); + break; + default: + throw new HTMLPurifier_Exception( + "Cannot instantiate unrecognized Lexer type " . + htmlspecialchars($lexer) + ); + } + } + + if (!$inst) { + throw new HTMLPurifier_Exception('No lexer was instantiated'); + } + + // once PHP DOM implements native line numbers, or we + // hack out something using XSLT, remove this stipulation + if ($needs_tracking && !$inst->tracksLineNumbers) { + throw new HTMLPurifier_Exception( + 'Cannot use lexer that does not support line numbers with ' . + 'Core.MaintainLineNumbers or Core.CollectErrors (use DirectLex instead)' + ); + } + + return $inst; + + } + + // -- CONVENIENCE MEMBERS --------------------------------------------- + + public function __construct() + { + $this->_entity_parser = new HTMLPurifier_EntityParser(); + } + + /** + * Most common entity to raw value conversion table for special entities. + * @type array + */ + protected $_special_entity2str = + array( + '"' => '"', + '&' => '&', + '<' => '<', + '>' => '>', + ''' => "'", + ''' => "'", + ''' => "'" + ); + + public function parseText($string, $config) { + return $this->parseData($string, false, $config); + } + + public function parseAttr($string, $config) { + return $this->parseData($string, true, $config); + } + + /** + * Parses special entities into the proper characters. + * + * This string will translate escaped versions of the special characters + * into the correct ones. + * + * @param string $string String character data to be parsed. + * @return string Parsed character data. + */ + public function parseData($string, $is_attr, $config) + { + // following functions require at least one character + if ($string === '') { + return ''; + } + + // subtracts amps that cannot possibly be escaped + $num_amp = substr_count($string, '&') - substr_count($string, '& ') - + ($string[strlen($string) - 1] === '&' ? 1 : 0); + + if (!$num_amp) { + return $string; + } // abort if no entities + $num_esc_amp = substr_count($string, '&'); + $string = strtr($string, $this->_special_entity2str); + + // code duplication for sake of optimization, see above + $num_amp_2 = substr_count($string, '&') - substr_count($string, '& ') - + ($string[strlen($string) - 1] === '&' ? 1 : 0); + + if ($num_amp_2 <= $num_esc_amp) { + return $string; + } + + // hmm... now we have some uncommon entities. Use the callback. + if ($config->get('Core.LegacyEntityDecoder')) { + $string = $this->_entity_parser->substituteSpecialEntities($string); + } else { + if ($is_attr) { + $string = $this->_entity_parser->substituteAttrEntities($string); + } else { + $string = $this->_entity_parser->substituteTextEntities($string); + } + } + return $string; + } + + /** + * Lexes an HTML string into tokens. + * @param $string String HTML. + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return HTMLPurifier_Token[] array representation of HTML. + */ + public function tokenizeHTML($string, $config, $context) + { + trigger_error('Call to abstract class', E_USER_ERROR); + } + + /** + * Translates CDATA sections into regular sections (through escaping). + * @param string $string HTML string to process. + * @return string HTML with CDATA sections escaped. + */ + protected static function escapeCDATA($string) + { + return preg_replace_callback( + '/<!\[CDATA\[(.+?)\]\]>/s', + array('HTMLPurifier_Lexer', 'CDATACallback'), + $string + ); + } + + /** + * Special CDATA case that is especially convoluted for <script> + * @param string $string HTML string to process. + * @return string HTML with CDATA sections escaped. + */ + protected static function escapeCommentedCDATA($string) + { + return preg_replace_callback( + '#<!--//--><!\[CDATA\[//><!--(.+?)//--><!\]\]>#s', + array('HTMLPurifier_Lexer', 'CDATACallback'), + $string + ); + } + + /** + * Special Internet Explorer conditional comments should be removed. + * @param string $string HTML string to process. + * @return string HTML with conditional comments removed. + */ + protected static function removeIEConditional($string) + { + return preg_replace( + '#<!--\[if [^>]+\]>.*?<!\[endif\]-->#si', // probably should generalize for all strings + '', + $string + ); + } + + /** + * Callback function for escapeCDATA() that does the work. + * + * @warning Though this is public in order to let the callback happen, + * calling it directly is not recommended. + * @param array $matches PCRE matches array, with index 0 the entire match + * and 1 the inside of the CDATA section. + * @return string Escaped internals of the CDATA section. + */ + protected static function CDATACallback($matches) + { + // not exactly sure why the character set is needed, but whatever + return htmlspecialchars($matches[1], ENT_COMPAT, 'UTF-8'); + } + + /** + * Takes a piece of HTML and normalizes it by converting entities, fixing + * encoding, extracting bits, and other good stuff. + * @param string $html HTML. + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + * @todo Consider making protected + */ + public function normalize($html, $config, $context) + { + // normalize newlines to \n + if ($config->get('Core.NormalizeNewlines')) { + $html = str_replace("\r\n", "\n", $html); + $html = str_replace("\r", "\n", $html); + } + + if ($config->get('HTML.Trusted')) { + // escape convoluted CDATA + $html = $this->escapeCommentedCDATA($html); + } + + // escape CDATA + $html = $this->escapeCDATA($html); + + $html = $this->removeIEConditional($html); + + // extract body from document if applicable + if ($config->get('Core.ConvertDocumentToFragment')) { + $e = false; + if ($config->get('Core.CollectErrors')) { + $e =& $context->get('ErrorCollector'); + } + $new_html = $this->extractBody($html); + if ($e && $new_html != $html) { + $e->send(E_WARNING, 'Lexer: Extracted body'); + } + $html = $new_html; + } + + // expand entities that aren't the big five + if ($config->get('Core.LegacyEntityDecoder')) { + $html = $this->_entity_parser->substituteNonSpecialEntities($html); + } + + // clean into wellformed UTF-8 string for an SGML context: this has + // to be done after entity expansion because the entities sometimes + // represent non-SGML characters (horror, horror!) + $html = HTMLPurifier_Encoder::cleanUTF8($html); + + // if processing instructions are to removed, remove them now + if ($config->get('Core.RemoveProcessingInstructions')) { + $html = preg_replace('#<\?.+?\?>#s', '', $html); + } + + $hidden_elements = $config->get('Core.HiddenElements'); + if ($config->get('Core.AggressivelyRemoveScript') && + !($config->get('HTML.Trusted') || !$config->get('Core.RemoveScriptContents') + || empty($hidden_elements["script"]))) { + $html = preg_replace('#<script[^>]*>.*?</script>#i', '', $html); + } + + return $html; + } + + /** + * Takes a string of HTML (fragment or document) and returns the content + * @todo Consider making protected + */ + public function extractBody($html) + { + $matches = array(); + $result = preg_match('|(.*?)<body[^>]*>(.*)</body>|is', $html, $matches); + if ($result) { + // Make sure it's not in a comment + $comment_start = strrpos($matches[1], '<!--'); + $comment_end = strrpos($matches[1], '-->'); + if ($comment_start === false || + ($comment_end !== false && $comment_end > $comment_start)) { + return $matches[2]; + } + } + return $html; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DOMLex.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DOMLex.php new file mode 100644 index 000000000..22ab5820c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DOMLex.php @@ -0,0 +1,291 @@ +<?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 + + set_error_handler(array($this, 'muteErrorHandler')); + $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); + } + + /** + * @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) { + $tokens[] = $this->factory->createText($node->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(); + + // We still have to make sure that the element actually IS empty + if (!$node->childNodes->length) { + if ($collect) { + $tokens[] = $this->factory->createEmpty($node->tagName, $attr); + } + return false; + } else { + if ($collect) { + $tokens[] = $this->factory->createStart( + $tag_name = $node->tagName, // somehow, it get's dropped + $attr + ); + } + return true; + } + } + + /** + * @param DOMNode $node + * @param HTMLPurifier_Token[] $tokens + */ + protected function createEndNode($node, &$tokens) + { + $tokens[] = $this->factory->createEnd($node->tagName); + } + + + /** + * 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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DirectLex.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DirectLex.php new file mode 100644 index 000000000..6f1308966 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DirectLex.php @@ -0,0 +1,539 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/PH5P.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/PH5P.php new file mode 100644 index 000000000..0b452d17f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/PH5P.php @@ -0,0 +1,4788 @@ +<?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 bellow. + 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 bellow. + 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'], $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; + } +} diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node.php new file mode 100644 index 000000000..3995fec9f --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node.php @@ -0,0 +1,49 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Comment.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Comment.php new file mode 100644 index 000000000..38ba19394 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Comment.php @@ -0,0 +1,36 @@ +<?php + +/** + * Concrete comment node class. + */ +class HTMLPurifier_Node_Comment extends HTMLPurifier_Node +{ + /** + * Character data within comment. + * @type string + */ + public $data; + + /** + * @type bool + */ + public $is_whitespace = true; + + /** + * Transparent constructor. + * + * @param string $data String comment data. + * @param int $line + * @param int $col + */ + public function __construct($data, $line = null, $col = null) + { + $this->data = $data; + $this->line = $line; + $this->col = $col; + } + + public function toTokenPair() { + return array(new HTMLPurifier_Token_Comment($this->data, $this->line, $this->col), null); + } +} diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Element.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Element.php new file mode 100644 index 000000000..6cbf56dad --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Element.php @@ -0,0 +1,59 @@ +<?php + +/** + * Concrete element node class. + */ +class HTMLPurifier_Node_Element extends HTMLPurifier_Node +{ + /** + * The lower-case name of the tag, like 'a', 'b' or 'blockquote'. + * + * @note Strictly speaking, XML tags are case sensitive, so we shouldn't + * be lower-casing them, but these tokens cater to HTML tags, which are + * insensitive. + * @type string + */ + public $name; + + /** + * Associative array of the node's attributes. + * @type array + */ + public $attr = array(); + + /** + * List of child elements. + * @type array + */ + public $children = array(); + + /** + * Does this use the <a></a> form or the </a> form, i.e. + * is it a pair of start/end tokens or an empty token. + * @bool + */ + public $empty = false; + + public $endCol = null, $endLine = null, $endArmor = array(); + + public function __construct($name, $attr = array(), $line = null, $col = null, $armor = array()) { + $this->name = $name; + $this->attr = $attr; + $this->line = $line; + $this->col = $col; + $this->armor = $armor; + } + + public function toTokenPair() { + // XXX inefficiency here, normalization is not necessary + if ($this->empty) { + return array(new HTMLPurifier_Token_Empty($this->name, $this->attr, $this->line, $this->col, $this->armor), null); + } else { + $start = new HTMLPurifier_Token_Start($this->name, $this->attr, $this->line, $this->col, $this->armor); + $end = new HTMLPurifier_Token_End($this->name, array(), $this->endLine, $this->endCol, $this->endArmor); + //$end->start = $start; + return array($start, $end); + } + } +} + diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Text.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Text.php new file mode 100644 index 000000000..aec916647 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Text.php @@ -0,0 +1,54 @@ +<?php + +/** + * Concrete text token class. + * + * Text tokens comprise of regular parsed character data (PCDATA) and raw + * character data (from the CDATA sections). Internally, their + * data is parsed with all entities expanded. Surprisingly, the text token + * does have a "tag name" called #PCDATA, which is how the DTD represents it + * in permissible child nodes. + */ +class HTMLPurifier_Node_Text extends HTMLPurifier_Node +{ + + /** + * PCDATA tag name compatible with DTD, see + * HTMLPurifier_ChildDef_Custom for details. + * @type string + */ + public $name = '#PCDATA'; + + /** + * @type string + */ + public $data; + /**< Parsed character data of text. */ + + /** + * @type bool + */ + public $is_whitespace; + + /**< Bool indicating if node is whitespace. */ + + /** + * Constructor, accepts data and determines if it is whitespace. + * @param string $data String parsed character data. + * @param int $line + * @param int $col + */ + public function __construct($data, $is_whitespace, $line = null, $col = null) + { + $this->data = $data; + $this->is_whitespace = $is_whitespace; + $this->line = $line; + $this->col = $col; + } + + public function toTokenPair() { + return array(new HTMLPurifier_Token_Text($this->data, $this->line, $this->col), null); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PercentEncoder.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PercentEncoder.php new file mode 100644 index 000000000..18c8bbb00 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PercentEncoder.php @@ -0,0 +1,111 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer.php new file mode 100644 index 000000000..549e4cea1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer.php @@ -0,0 +1,218 @@ +<?php + +// OUT OF DATE, NEEDS UPDATING! +// USE XMLWRITER! + +class HTMLPurifier_Printer +{ + + /** + * For HTML generation convenience funcs. + * @type HTMLPurifier_Generator + */ + protected $generator; + + /** + * For easy access. + * @type HTMLPurifier_Config + */ + protected $config; + + /** + * Initialize $generator. + */ + public function __construct() + { + } + + /** + * Give generator necessary configuration if possible + * @param HTMLPurifier_Config $config + */ + public function prepareGenerator($config) + { + $all = $config->getAll(); + $context = new HTMLPurifier_Context(); + $this->generator = new HTMLPurifier_Generator($config, $context); + } + + /** + * Main function that renders object or aspect of that object + * @note Parameters vary depending on printer + */ + // function render() {} + + /** + * Returns a start tag + * @param string $tag Tag name + * @param array $attr Attribute array + * @return string + */ + protected function start($tag, $attr = array()) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Start($tag, $attr ? $attr : array()) + ); + } + + /** + * Returns an end tag + * @param string $tag Tag name + * @return string + */ + protected function end($tag) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_End($tag) + ); + } + + /** + * Prints a complete element with content inside + * @param string $tag Tag name + * @param string $contents Element contents + * @param array $attr Tag attributes + * @param bool $escape whether or not to escape contents + * @return string + */ + protected function element($tag, $contents, $attr = array(), $escape = true) + { + return $this->start($tag, $attr) . + ($escape ? $this->escape($contents) : $contents) . + $this->end($tag); + } + + /** + * @param string $tag + * @param array $attr + * @return string + */ + protected function elementEmpty($tag, $attr = array()) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Empty($tag, $attr) + ); + } + + /** + * @param string $text + * @return string + */ + protected function text($text) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Text($text) + ); + } + + /** + * Prints a simple key/value row in a table. + * @param string $name Key + * @param mixed $value Value + * @return string + */ + protected function row($name, $value) + { + if (is_bool($value)) { + $value = $value ? 'On' : 'Off'; + } + return + $this->start('tr') . "\n" . + $this->element('th', $name) . "\n" . + $this->element('td', $value) . "\n" . + $this->end('tr'); + } + + /** + * Escapes a string for HTML output. + * @param string $string String to escape + * @return string + */ + protected function escape($string) + { + $string = HTMLPurifier_Encoder::cleanUTF8($string); + $string = htmlspecialchars($string, ENT_COMPAT, 'UTF-8'); + return $string; + } + + /** + * Takes a list of strings and turns them into a single list + * @param string[] $array List of strings + * @param bool $polite Bool whether or not to add an end before the last + * @return string + */ + protected function listify($array, $polite = false) + { + if (empty($array)) { + return 'None'; + } + $ret = ''; + $i = count($array); + foreach ($array as $value) { + $i--; + $ret .= $value; + if ($i > 0 && !($polite && $i == 1)) { + $ret .= ', '; + } + if ($polite && $i == 1) { + $ret .= 'and '; + } + } + return $ret; + } + + /** + * Retrieves the class of an object without prefixes, as well as metadata + * @param object $obj Object to determine class of + * @param string $sec_prefix Further prefix to remove + * @return string + */ + protected function getClass($obj, $sec_prefix = '') + { + static $five = null; + if ($five === null) { + $five = version_compare(PHP_VERSION, '5', '>='); + } + $prefix = 'HTMLPurifier_' . $sec_prefix; + if (!$five) { + $prefix = strtolower($prefix); + } + $class = str_replace($prefix, '', get_class($obj)); + $lclass = strtolower($class); + $class .= '('; + switch ($lclass) { + case 'enum': + $values = array(); + foreach ($obj->valid_values as $value => $bool) { + $values[] = $value; + } + $class .= implode(', ', $values); + break; + case 'css_composite': + $values = array(); + foreach ($obj->defs as $def) { + $values[] = $this->getClass($def, $sec_prefix); + } + $class .= implode(', ', $values); + break; + case 'css_multiple': + $class .= $this->getClass($obj->single, $sec_prefix) . ', '; + $class .= $obj->max; + break; + case 'css_denyelementdecorator': + $class .= $this->getClass($obj->def, $sec_prefix) . ', '; + $class .= $obj->element; + break; + case 'css_importantdecorator': + $class .= $this->getClass($obj->def, $sec_prefix); + if ($obj->allow) { + $class .= ', !important'; + } + break; + } + $class .= ')'; + return $class; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/CSSDefinition.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/CSSDefinition.php new file mode 100644 index 000000000..29505fe12 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/CSSDefinition.php @@ -0,0 +1,44 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.css b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.css new file mode 100644 index 000000000..3ff1a88aa --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.css @@ -0,0 +1,10 @@ + +.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 */ diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.js b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.js new file mode 100644 index 000000000..cba00c9b8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.js @@ -0,0 +1,5 @@ +function toggleWriteability(id_of_patient, checked) { + document.getElementById(id_of_patient).disabled = checked; +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.php new file mode 100644 index 000000000..65a777904 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.php @@ -0,0 +1,451 @@ +<?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::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::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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/HTMLDefinition.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/HTMLDefinition.php new file mode 100644 index 000000000..5f2f2f8a7 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/HTMLDefinition.php @@ -0,0 +1,324 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PropertyList.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PropertyList.php new file mode 100644 index 000000000..189348fd9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PropertyList.php @@ -0,0 +1,122 @@ +<?php + +/** + * Generic property list implementation + */ +class HTMLPurifier_PropertyList +{ + /** + * Internal data-structure for properties. + * @type array + */ + protected $data = array(); + + /** + * Parent plist. + * @type HTMLPurifier_PropertyList + */ + protected $parent; + + /** + * Cache. + * @type array + */ + protected $cache; + + /** + * @param HTMLPurifier_PropertyList $parent Parent plist + */ + public function __construct($parent = null) + { + $this->parent = $parent; + } + + /** + * Recursively retrieves the value for a key + * @param string $name + * @throws HTMLPurifier_Exception + */ + public function get($name) + { + if ($this->has($name)) { + return $this->data[$name]; + } + // possible performance bottleneck, convert to iterative if necessary + if ($this->parent) { + return $this->parent->get($name); + } + throw new HTMLPurifier_Exception("Key '$name' not found"); + } + + /** + * Sets the value of a key, for this plist + * @param string $name + * @param mixed $value + */ + public function set($name, $value) + { + $this->data[$name] = $value; + } + + /** + * Returns true if a given key exists + * @param string $name + * @return bool + */ + public function has($name) + { + return array_key_exists($name, $this->data); + } + + /** + * Resets a value to the value of it's parent, usually the default. If + * no value is specified, the entire plist is reset. + * @param string $name + */ + public function reset($name = null) + { + if ($name == null) { + $this->data = array(); + } else { + unset($this->data[$name]); + } + } + + /** + * Squashes this property list and all of its property lists into a single + * array, and returns the array. This value is cached by default. + * @param bool $force If true, ignores the cache and regenerates the array. + * @return array + */ + public function squash($force = false) + { + if ($this->cache !== null && !$force) { + return $this->cache; + } + if ($this->parent) { + return $this->cache = array_merge($this->parent->squash($force), $this->data); + } else { + return $this->cache = $this->data; + } + } + + /** + * Returns the parent plist. + * @return HTMLPurifier_PropertyList + */ + public function getParent() + { + return $this->parent; + } + + /** + * Sets the parent plist. + * @param HTMLPurifier_PropertyList $plist Parent plist + */ + public function setParent($plist) + { + $this->parent = $plist; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PropertyListIterator.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PropertyListIterator.php new file mode 100644 index 000000000..15b330ea3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PropertyListIterator.php @@ -0,0 +1,42 @@ +<?php + +/** + * Property list iterator. Do not instantiate this class directly. + */ +class HTMLPurifier_PropertyListIterator extends FilterIterator +{ + + /** + * @type int + */ + protected $l; + /** + * @type string + */ + protected $filter; + + /** + * @param Iterator $iterator Array of data to iterate over + * @param string $filter Optional prefix to only allow values of + */ + public function __construct(Iterator $iterator, $filter = null) + { + parent::__construct($iterator); + $this->l = strlen($filter); + $this->filter = $filter; + } + + /** + * @return bool + */ + public function accept() + { + $key = $this->getInnerIterator()->key(); + if (strncmp($key, $this->filter, $this->l) !== 0) { + return false; + } + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Queue.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Queue.php new file mode 100644 index 000000000..f58db9042 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Queue.php @@ -0,0 +1,56 @@ +<?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); + } +} diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy.php new file mode 100644 index 000000000..e1ff3b72d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy.php @@ -0,0 +1,26 @@ +<?php + +/** + * Supertype for classes that define a strategy for modifying/purifying tokens. + * + * While HTMLPurifier's core purpose is fixing HTML into something proper, + * strategies provide plug points for extra configuration or even extra + * features, such as custom tags, custom parsing of text, etc. + */ + + +abstract class HTMLPurifier_Strategy +{ + + /** + * Executes the strategy on the tokens. + * + * @param HTMLPurifier_Token[] $tokens Array of HTMLPurifier_Token objects to be operated on. + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return HTMLPurifier_Token[] Processed array of token objects. + */ + abstract public function execute($tokens, $config, $context); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/Composite.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/Composite.php new file mode 100644 index 000000000..d7d35ce7d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/Composite.php @@ -0,0 +1,30 @@ +<?php + +/** + * Composite strategy that runs multiple strategies on tokens. + */ +abstract class HTMLPurifier_Strategy_Composite extends HTMLPurifier_Strategy +{ + + /** + * List of strategies to run tokens through. + * @type HTMLPurifier_Strategy[] + */ + protected $strategies = array(); + + /** + * @param HTMLPurifier_Token[] $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return HTMLPurifier_Token[] + */ + public function execute($tokens, $config, $context) + { + foreach ($this->strategies as $strategy) { + $tokens = $strategy->execute($tokens, $config, $context); + } + return $tokens; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/Core.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/Core.php new file mode 100644 index 000000000..4414c17d6 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/Core.php @@ -0,0 +1,17 @@ +<?php + +/** + * Core strategy composed of the big four strategies. + */ +class HTMLPurifier_Strategy_Core extends HTMLPurifier_Strategy_Composite +{ + public function __construct() + { + $this->strategies[] = new HTMLPurifier_Strategy_RemoveForeignElements(); + $this->strategies[] = new HTMLPurifier_Strategy_MakeWellFormed(); + $this->strategies[] = new HTMLPurifier_Strategy_FixNesting(); + $this->strategies[] = new HTMLPurifier_Strategy_ValidateAttributes(); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/FixNesting.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/FixNesting.php new file mode 100644 index 000000000..6fa673db9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/FixNesting.php @@ -0,0 +1,181 @@ +<?php + +/** + * Takes a well formed list of tokens and fixes their nesting. + * + * HTML elements dictate which elements are allowed to be their children, + * for example, you can't have a p tag in a span tag. Other elements have + * much more rigorous definitions: tables, for instance, require a specific + * order for their elements. There are also constraints not expressible by + * document type definitions, such as the chameleon nature of ins/del + * tags and global child exclusions. + * + * The first major objective of this strategy is to iterate through all + * the nodes and determine whether or not their children conform to the + * element's definition. If they do not, the child definition may + * optionally supply an amended list of elements that is valid or + * require that the entire node be deleted (and the previous node + * rescanned). + * + * The second objective is to ensure that explicitly excluded elements of + * an element do not appear in its children. Code that accomplishes this + * task is pervasive through the strategy, though the two are distinct tasks + * and could, theoretically, be seperated (although it's not recommended). + * + * @note Whether or not unrecognized children are silently dropped or + * translated into text depends on the child definitions. + * + * @todo Enable nodes to be bubbled out of the structure. This is + * easier with our new algorithm. + */ + +class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy +{ + + /** + * @param HTMLPurifier_Token[] $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array|HTMLPurifier_Token[] + */ + public function execute($tokens, $config, $context) + { + + //####################################################################// + // Pre-processing + + // O(n) pass to convert to a tree, so that we can efficiently + // refer to substrings + $top_node = HTMLPurifier_Arborize::arborize($tokens, $config, $context); + + // get a copy of the HTML definition + $definition = $config->getHTMLDefinition(); + + $excludes_enabled = !$config->get('Core.DisableExcludes'); + + // setup the context variable 'IsInline', for chameleon processing + // is 'false' when we are not inline, 'true' when it must always + // be inline, and an integer when it is inline for a certain + // branch of the document tree + $is_inline = $definition->info_parent_def->descendants_are_inline; + $context->register('IsInline', $is_inline); + + // setup error collector + $e =& $context->get('ErrorCollector', true); + + //####################################################################// + // Loop initialization + + // stack that contains all elements that are excluded + // it is organized by parent elements, similar to $stack, + // but it is only populated when an element with exclusions is + // processed, i.e. there won't be empty exclusions. + $exclude_stack = array($definition->info_parent_def->excludes); + + // variable that contains the start token while we are processing + // nodes. This enables error reporting to do its job + $node = $top_node; + // dummy token + list($token, $d) = $node->toTokenPair(); + $context->register('CurrentNode', $node); + $context->register('CurrentToken', $token); + + //####################################################################// + // Loop + + // We need to implement a post-order traversal iteratively, to + // avoid running into stack space limits. This is pretty tricky + // to reason about, so we just manually stack-ify the recursive + // variant: + // + // function f($node) { + // foreach ($node->children as $child) { + // f($child); + // } + // validate($node); + // } + // + // Thus, we will represent a stack frame as array($node, + // $is_inline, stack of children) + // e.g. array_reverse($node->children) - already processed + // children. + + $parent_def = $definition->info_parent_def; + $stack = array( + array($top_node, + $parent_def->descendants_are_inline, + $parent_def->excludes, // exclusions + 0) + ); + + while (!empty($stack)) { + list($node, $is_inline, $excludes, $ix) = array_pop($stack); + // recursive call + $go = false; + $def = empty($stack) ? $definition->info_parent_def : $definition->info[$node->name]; + while (isset($node->children[$ix])) { + $child = $node->children[$ix++]; + if ($child instanceof HTMLPurifier_Node_Element) { + $go = true; + $stack[] = array($node, $is_inline, $excludes, $ix); + $stack[] = array($child, + // ToDo: I don't think it matters if it's def or + // child_def, but double check this... + $is_inline || $def->descendants_are_inline, + empty($def->excludes) ? $excludes + : array_merge($excludes, $def->excludes), + 0); + break; + } + }; + if ($go) continue; + list($token, $d) = $node->toTokenPair(); + // base case + if ($excludes_enabled && isset($excludes[$node->name])) { + $node->dead = true; + if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node excluded'); + } else { + // XXX I suppose it would be slightly more efficient to + // avoid the allocation here and have children + // strategies handle it + $children = array(); + foreach ($node->children as $child) { + if (!$child->dead) $children[] = $child; + } + $result = $def->child->validateChildren($children, $config, $context); + if ($result === true) { + // nop + $node->children = $children; + } elseif ($result === false) { + $node->dead = true; + if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node removed'); + } else { + $node->children = $result; + if ($e) { + // XXX This will miss mutations of internal nodes. Perhaps defer to the child validators + if (empty($result) && !empty($children)) { + $e->send(E_ERROR, 'Strategy_FixNesting: Node contents removed'); + } else if ($result != $children) { + $e->send(E_WARNING, 'Strategy_FixNesting: Node reorganized'); + } + } + } + } + } + + //####################################################################// + // Post-processing + + // remove context variables + $context->destroy('IsInline'); + $context->destroy('CurrentNode'); + $context->destroy('CurrentToken'); + + //####################################################################// + // Return + + return HTMLPurifier_Arborize::flatten($node, $config, $context); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/MakeWellFormed.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/MakeWellFormed.php new file mode 100644 index 000000000..a6eb09e45 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/MakeWellFormed.php @@ -0,0 +1,659 @@ +<?php + +/** + * Takes tokens makes them well-formed (balance end tags, etc.) + * + * Specification of the armor attributes this strategy uses: + * + * - MakeWellFormed_TagClosedError: This armor field is used to + * suppress tag closed errors for certain tokens [TagClosedSuppress], + * in particular, if a tag was generated automatically by HTML + * Purifier, we may rely on our infrastructure to close it for us + * and shouldn't report an error to the user [TagClosedAuto]. + */ +class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy +{ + + /** + * Array stream of tokens being processed. + * @type HTMLPurifier_Token[] + */ + protected $tokens; + + /** + * Current token. + * @type HTMLPurifier_Token + */ + protected $token; + + /** + * Zipper managing the true state. + * @type HTMLPurifier_Zipper + */ + protected $zipper; + + /** + * Current nesting of elements. + * @type array + */ + protected $stack; + + /** + * Injectors active in this stream processing. + * @type HTMLPurifier_Injector[] + */ + protected $injectors; + + /** + * Current instance of HTMLPurifier_Config. + * @type HTMLPurifier_Config + */ + protected $config; + + /** + * Current instance of HTMLPurifier_Context. + * @type HTMLPurifier_Context + */ + protected $context; + + /** + * @param HTMLPurifier_Token[] $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return HTMLPurifier_Token[] + * @throws HTMLPurifier_Exception + */ + public function execute($tokens, $config, $context) + { + $definition = $config->getHTMLDefinition(); + + // local variables + $generator = new HTMLPurifier_Generator($config, $context); + $escape_invalid_tags = $config->get('Core.EscapeInvalidTags'); + // used for autoclose early abortion + $global_parent_allowed_elements = $definition->info_parent_def->child->getAllowedElements($config); + $e = $context->get('ErrorCollector', true); + $i = false; // injector index + list($zipper, $token) = HTMLPurifier_Zipper::fromArray($tokens); + if ($token === NULL) { + return array(); + } + $reprocess = false; // whether or not to reprocess the same token + $stack = array(); + + // member variables + $this->stack =& $stack; + $this->tokens =& $tokens; + $this->token =& $token; + $this->zipper =& $zipper; + $this->config = $config; + $this->context = $context; + + // context variables + $context->register('CurrentNesting', $stack); + $context->register('InputZipper', $zipper); + $context->register('CurrentToken', $token); + + // -- begin INJECTOR -- + + $this->injectors = array(); + + $injectors = $config->getBatch('AutoFormat'); + $def_injectors = $definition->info_injector; + $custom_injectors = $injectors['Custom']; + unset($injectors['Custom']); // special case + foreach ($injectors as $injector => $b) { + // XXX: Fix with a legitimate lookup table of enabled filters + if (strpos($injector, '.') !== false) { + continue; + } + $injector = "HTMLPurifier_Injector_$injector"; + if (!$b) { + continue; + } + $this->injectors[] = new $injector; + } + foreach ($def_injectors as $injector) { + // assumed to be objects + $this->injectors[] = $injector; + } + foreach ($custom_injectors as $injector) { + if (!$injector) { + continue; + } + if (is_string($injector)) { + $injector = "HTMLPurifier_Injector_$injector"; + $injector = new $injector; + } + $this->injectors[] = $injector; + } + + // give the injectors references to the definition and context + // variables for performance reasons + foreach ($this->injectors as $ix => $injector) { + $error = $injector->prepare($config, $context); + if (!$error) { + continue; + } + array_splice($this->injectors, $ix, 1); // rm the injector + trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING); + } + + // -- end INJECTOR -- + + // a note on reprocessing: + // In order to reduce code duplication, whenever some code needs + // to make HTML changes in order to make things "correct", the + // new HTML gets sent through the purifier, regardless of its + // status. This means that if we add a start token, because it + // was totally necessary, we don't have to update nesting; we just + // punt ($reprocess = true; continue;) and it does that for us. + + // isset is in loop because $tokens size changes during loop exec + for (;; + // only increment if we don't need to reprocess + $reprocess ? $reprocess = false : $token = $zipper->next($token)) { + + // check for a rewind + if (is_int($i)) { + // possibility: disable rewinding if the current token has a + // rewind set on it already. This would offer protection from + // infinite loop, but might hinder some advanced rewinding. + $rewind_offset = $this->injectors[$i]->getRewindOffset(); + if (is_int($rewind_offset)) { + for ($j = 0; $j < $rewind_offset; $j++) { + if (empty($zipper->front)) break; + $token = $zipper->prev($token); + // indicate that other injectors should not process this token, + // but we need to reprocess it. See Note [Injector skips] + unset($token->skip[$i]); + $token->rewind = $i; + if ($token instanceof HTMLPurifier_Token_Start) { + array_pop($this->stack); + } elseif ($token instanceof HTMLPurifier_Token_End) { + $this->stack[] = $token->start; + } + } + } + $i = false; + } + + // handle case of document end + if ($token === NULL) { + // kill processing if stack is empty + if (empty($this->stack)) { + break; + } + + // peek + $top_nesting = array_pop($this->stack); + $this->stack[] = $top_nesting; + + // send error [TagClosedSuppress] + if ($e && !isset($top_nesting->armor['MakeWellFormed_TagClosedError'])) { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $top_nesting); + } + + // append, don't splice, since this is the end + $token = new HTMLPurifier_Token_End($top_nesting->name); + + // punt! + $reprocess = true; + continue; + } + + //echo '<br>'; printZipper($zipper, $token);//printTokens($this->stack); + //flush(); + + // quick-check: if it's not a tag, no need to process + if (empty($token->is_tag)) { + if ($token instanceof HTMLPurifier_Token_Text) { + foreach ($this->injectors as $i => $injector) { + if (isset($token->skip[$i])) { + // See Note [Injector skips] + continue; + } + if ($token->rewind !== null && $token->rewind !== $i) { + continue; + } + // XXX fuckup + $r = $token; + $injector->handleText($r); + $token = $this->processToken($r, $i); + $reprocess = true; + break; + } + } + // another possibility is a comment + continue; + } + + if (isset($definition->info[$token->name])) { + $type = $definition->info[$token->name]->child->type; + } else { + $type = false; // Type is unknown, treat accordingly + } + + // quick tag checks: anything that's *not* an end tag + $ok = false; + if ($type === 'empty' && $token instanceof HTMLPurifier_Token_Start) { + // claims to be a start tag but is empty + $token = new HTMLPurifier_Token_Empty( + $token->name, + $token->attr, + $token->line, + $token->col, + $token->armor + ); + $ok = true; + } elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) { + // claims to be empty but really is a start tag + // NB: this assignment is required + $old_token = $token; + $token = new HTMLPurifier_Token_End($token->name); + $token = $this->insertBefore( + new HTMLPurifier_Token_Start($old_token->name, $old_token->attr, $old_token->line, $old_token->col, $old_token->armor) + ); + // punt (since we had to modify the input stream in a non-trivial way) + $reprocess = true; + continue; + } elseif ($token instanceof HTMLPurifier_Token_Empty) { + // real empty token + $ok = true; + } elseif ($token instanceof HTMLPurifier_Token_Start) { + // start tag + + // ...unless they also have to close their parent + if (!empty($this->stack)) { + + // Performance note: you might think that it's rather + // inefficient, recalculating the autoclose information + // for every tag that a token closes (since when we + // do an autoclose, we push a new token into the + // stream and then /process/ that, before + // re-processing this token.) But this is + // necessary, because an injector can make an + // arbitrary transformations to the autoclosing + // tokens we introduce, so things may have changed + // in the meantime. Also, doing the inefficient thing is + // "easy" to reason about (for certain perverse definitions + // of "easy") + + $parent = array_pop($this->stack); + $this->stack[] = $parent; + + $parent_def = null; + $parent_elements = null; + $autoclose = false; + if (isset($definition->info[$parent->name])) { + $parent_def = $definition->info[$parent->name]; + $parent_elements = $parent_def->child->getAllowedElements($config); + $autoclose = !isset($parent_elements[$token->name]); + } + + if ($autoclose && $definition->info[$token->name]->wrap) { + // Check if an element can be wrapped by another + // element to make it valid in a context (for + // example, <ul><ul> needs a <li> in between) + $wrapname = $definition->info[$token->name]->wrap; + $wrapdef = $definition->info[$wrapname]; + $elements = $wrapdef->child->getAllowedElements($config); + if (isset($elements[$token->name]) && isset($parent_elements[$wrapname])) { + $newtoken = new HTMLPurifier_Token_Start($wrapname); + $token = $this->insertBefore($newtoken); + $reprocess = true; + continue; + } + } + + $carryover = false; + if ($autoclose && $parent_def->formatting) { + $carryover = true; + } + + if ($autoclose) { + // check if this autoclose is doomed to fail + // (this rechecks $parent, which his harmless) + $autoclose_ok = isset($global_parent_allowed_elements[$token->name]); + if (!$autoclose_ok) { + foreach ($this->stack as $ancestor) { + $elements = $definition->info[$ancestor->name]->child->getAllowedElements($config); + if (isset($elements[$token->name])) { + $autoclose_ok = true; + break; + } + if ($definition->info[$token->name]->wrap) { + $wrapname = $definition->info[$token->name]->wrap; + $wrapdef = $definition->info[$wrapname]; + $wrap_elements = $wrapdef->child->getAllowedElements($config); + if (isset($wrap_elements[$token->name]) && isset($elements[$wrapname])) { + $autoclose_ok = true; + break; + } + } + } + } + if ($autoclose_ok) { + // errors need to be updated + $new_token = new HTMLPurifier_Token_End($parent->name); + $new_token->start = $parent; + // [TagClosedSuppress] + if ($e && !isset($parent->armor['MakeWellFormed_TagClosedError'])) { + if (!$carryover) { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent); + } else { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag carryover', $parent); + } + } + if ($carryover) { + $element = clone $parent; + // [TagClosedAuto] + $element->armor['MakeWellFormed_TagClosedError'] = true; + $element->carryover = true; + $token = $this->processToken(array($new_token, $token, $element)); + } else { + $token = $this->insertBefore($new_token); + } + } else { + $token = $this->remove(); + } + $reprocess = true; + continue; + } + + } + $ok = true; + } + + if ($ok) { + foreach ($this->injectors as $i => $injector) { + if (isset($token->skip[$i])) { + // See Note [Injector skips] + continue; + } + if ($token->rewind !== null && $token->rewind !== $i) { + continue; + } + $r = $token; + $injector->handleElement($r); + $token = $this->processToken($r, $i); + $reprocess = true; + break; + } + if (!$reprocess) { + // ah, nothing interesting happened; do normal processing + if ($token instanceof HTMLPurifier_Token_Start) { + $this->stack[] = $token; + } elseif ($token instanceof HTMLPurifier_Token_End) { + throw new HTMLPurifier_Exception( + 'Improper handling of end tag in start code; possible error in MakeWellFormed' + ); + } + } + continue; + } + + // sanity check: we should be dealing with a closing tag + if (!$token instanceof HTMLPurifier_Token_End) { + throw new HTMLPurifier_Exception('Unaccounted for tag token in input stream, bug in HTML Purifier'); + } + + // make sure that we have something open + if (empty($this->stack)) { + if ($escape_invalid_tags) { + if ($e) { + $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag to text'); + } + $token = new HTMLPurifier_Token_Text($generator->generateFromToken($token)); + } else { + if ($e) { + $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed'); + } + $token = $this->remove(); + } + $reprocess = true; + continue; + } + + // first, check for the simplest case: everything closes neatly. + // Eventually, everything passes through here; if there are problems + // we modify the input stream accordingly and then punt, so that + // the tokens get processed again. + $current_parent = array_pop($this->stack); + if ($current_parent->name == $token->name) { + $token->start = $current_parent; + foreach ($this->injectors as $i => $injector) { + if (isset($token->skip[$i])) { + // See Note [Injector skips] + continue; + } + if ($token->rewind !== null && $token->rewind !== $i) { + continue; + } + $r = $token; + $injector->handleEnd($r); + $token = $this->processToken($r, $i); + $this->stack[] = $current_parent; + $reprocess = true; + break; + } + continue; + } + + // okay, so we're trying to close the wrong tag + + // undo the pop previous pop + $this->stack[] = $current_parent; + + // scroll back the entire nest, trying to find our tag. + // (feature could be to specify how far you'd like to go) + $size = count($this->stack); + // -2 because -1 is the last element, but we already checked that + $skipped_tags = false; + for ($j = $size - 2; $j >= 0; $j--) { + if ($this->stack[$j]->name == $token->name) { + $skipped_tags = array_slice($this->stack, $j); + break; + } + } + + // we didn't find the tag, so remove + if ($skipped_tags === false) { + if ($escape_invalid_tags) { + if ($e) { + $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text'); + } + $token = new HTMLPurifier_Token_Text($generator->generateFromToken($token)); + } else { + if ($e) { + $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed'); + } + $token = $this->remove(); + } + $reprocess = true; + continue; + } + + // do errors, in REVERSE $j order: a,b,c with </a></b></c> + $c = count($skipped_tags); + if ($e) { + for ($j = $c - 1; $j > 0; $j--) { + // notice we exclude $j == 0, i.e. the current ending tag, from + // the errors... [TagClosedSuppress] + if (!isset($skipped_tags[$j]->armor['MakeWellFormed_TagClosedError'])) { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by element end', $skipped_tags[$j]); + } + } + } + + // insert tags, in FORWARD $j order: c,b,a with </a></b></c> + $replace = array($token); + for ($j = 1; $j < $c; $j++) { + // ...as well as from the insertions + $new_token = new HTMLPurifier_Token_End($skipped_tags[$j]->name); + $new_token->start = $skipped_tags[$j]; + array_unshift($replace, $new_token); + if (isset($definition->info[$new_token->name]) && $definition->info[$new_token->name]->formatting) { + // [TagClosedAuto] + $element = clone $skipped_tags[$j]; + $element->carryover = true; + $element->armor['MakeWellFormed_TagClosedError'] = true; + $replace[] = $element; + } + } + $token = $this->processToken($replace); + $reprocess = true; + continue; + } + + $context->destroy('CurrentToken'); + $context->destroy('CurrentNesting'); + $context->destroy('InputZipper'); + + unset($this->injectors, $this->stack, $this->tokens); + return $zipper->toArray($token); + } + + /** + * Processes arbitrary token values for complicated substitution patterns. + * In general: + * + * If $token is an array, it is a list of tokens to substitute for the + * current token. These tokens then get individually processed. If there + * is a leading integer in the list, that integer determines how many + * tokens from the stream should be removed. + * + * If $token is a regular token, it is swapped with the current token. + * + * If $token is false, the current token is deleted. + * + * If $token is an integer, that number of tokens (with the first token + * being the current one) will be deleted. + * + * @param HTMLPurifier_Token|array|int|bool $token Token substitution value + * @param HTMLPurifier_Injector|int $injector Injector that performed the substitution; default is if + * this is not an injector related operation. + * @throws HTMLPurifier_Exception + */ + protected function processToken($token, $injector = -1) + { + // Zend OpCache miscompiles $token = array($token), so + // avoid this pattern. See: https://github.com/ezyang/htmlpurifier/issues/108 + + // normalize forms of token + if (is_object($token)) { + $tmp = $token; + $token = array(1, $tmp); + } + if (is_int($token)) { + $tmp = $token; + $token = array($tmp); + } + if ($token === false) { + $token = array(1); + } + if (!is_array($token)) { + throw new HTMLPurifier_Exception('Invalid token type from injector'); + } + if (!is_int($token[0])) { + array_unshift($token, 1); + } + if ($token[0] === 0) { + throw new HTMLPurifier_Exception('Deleting zero tokens is not valid'); + } + + // $token is now an array with the following form: + // array(number nodes to delete, new node 1, new node 2, ...) + + $delete = array_shift($token); + list($old, $r) = $this->zipper->splice($this->token, $delete, $token); + + if ($injector > -1) { + // See Note [Injector skips] + // Determine appropriate skips. Here's what the code does: + // *If* we deleted one or more tokens, copy the skips + // of those tokens into the skips of the new tokens (in $token). + // Also, mark the newly inserted tokens as having come from + // $injector. + $oldskip = isset($old[0]) ? $old[0]->skip : array(); + foreach ($token as $object) { + $object->skip = $oldskip; + $object->skip[$injector] = true; + } + } + + return $r; + + } + + /** + * Inserts a token before the current token. Cursor now points to + * this token. You must reprocess after this. + * @param HTMLPurifier_Token $token + */ + private function insertBefore($token) + { + // NB not $this->zipper->insertBefore(), due to positioning + // differences + $splice = $this->zipper->splice($this->token, 0, array($token)); + + return $splice[1]; + } + + /** + * Removes current token. Cursor now points to new token occupying previously + * occupied space. You must reprocess after this. + */ + private function remove() + { + return $this->zipper->delete(); + } +} + +// Note [Injector skips] +// ~~~~~~~~~~~~~~~~~~~~~ +// When I originally designed this class, the idea behind the 'skip' +// property of HTMLPurifier_Token was to help avoid infinite loops +// in injector processing. For example, suppose you wrote an injector +// that bolded swear words. Naively, you might write it so that +// whenever you saw ****, you replaced it with <strong>****</strong>. +// +// When this happens, we will reprocess all of the tokens with the +// other injectors. Now there is an opportunity for infinite loop: +// if we rerun the swear-word injector on these tokens, we might +// see **** and then reprocess again to get +// <strong><strong>****</strong></strong> ad infinitum. +// +// Thus, the idea of a skip is that once we process a token with +// an injector, we mark all of those tokens as having "come from" +// the injector, and we never run the injector again on these +// tokens. +// +// There were two more complications, however: +// +// - With HTMLPurifier_Injector_RemoveEmpty, we noticed that if +// you had <b><i></i></b>, after you removed the <i></i>, you +// really would like this injector to go back and reprocess +// the <b> tag, discovering that it is now empty and can be +// removed. So we reintroduced the possibility of infinite looping +// by adding a "rewind" function, which let you go back to an +// earlier point in the token stream and reprocess it with injectors. +// Needless to say, we need to UN-skip the token so it gets +// reprocessed. +// +// - Suppose that you successfuly process a token, replace it with +// one with your skip mark, but now another injector wants to +// process the skipped token with another token. Should you continue +// to skip that new token, or reprocess it? If you reprocess, +// you can end up with an infinite loop where one injector converts +// <a> to <b>, and then another injector converts it back. So +// we inherit the skips, but for some reason, I thought that we +// should inherit the skip from the first token of the token +// that we deleted. Why? Well, it seems to work OK. +// +// If I were to redesign this functionality, I would absolutely not +// go about doing it this way: the semantics are just not very well +// defined, and in any case you probably wanted to operate on trees, +// not token streams. + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/RemoveForeignElements.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/RemoveForeignElements.php new file mode 100644 index 000000000..1a8149ecc --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/RemoveForeignElements.php @@ -0,0 +1,207 @@ +<?php + +/** + * Removes all unrecognized tags from the list of tokens. + * + * This strategy iterates through all the tokens and removes unrecognized + * tokens. If a token is not recognized but a TagTransform is defined for + * that element, the element will be transformed accordingly. + */ + +class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy +{ + + /** + * @param HTMLPurifier_Token[] $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array|HTMLPurifier_Token[] + */ + public function execute($tokens, $config, $context) + { + $definition = $config->getHTMLDefinition(); + $generator = new HTMLPurifier_Generator($config, $context); + $result = array(); + + $escape_invalid_tags = $config->get('Core.EscapeInvalidTags'); + $remove_invalid_img = $config->get('Core.RemoveInvalidImg'); + + // currently only used to determine if comments should be kept + $trusted = $config->get('HTML.Trusted'); + $comment_lookup = $config->get('HTML.AllowedComments'); + $comment_regexp = $config->get('HTML.AllowedCommentsRegexp'); + $check_comments = $comment_lookup !== array() || $comment_regexp !== null; + + $remove_script_contents = $config->get('Core.RemoveScriptContents'); + $hidden_elements = $config->get('Core.HiddenElements'); + + // remove script contents compatibility + if ($remove_script_contents === true) { + $hidden_elements['script'] = true; + } elseif ($remove_script_contents === false && isset($hidden_elements['script'])) { + unset($hidden_elements['script']); + } + + $attr_validator = new HTMLPurifier_AttrValidator(); + + // removes tokens until it reaches a closing tag with its value + $remove_until = false; + + // converts comments into text tokens when this is equal to a tag name + $textify_comments = false; + + $token = false; + $context->register('CurrentToken', $token); + + $e = false; + if ($config->get('Core.CollectErrors')) { + $e =& $context->get('ErrorCollector'); + } + + foreach ($tokens as $token) { + if ($remove_until) { + if (empty($token->is_tag) || $token->name !== $remove_until) { + continue; + } + } + if (!empty($token->is_tag)) { + // DEFINITION CALL + + // before any processing, try to transform the element + if (isset($definition->info_tag_transform[$token->name])) { + $original_name = $token->name; + // there is a transformation for this tag + // DEFINITION CALL + $token = $definition-> + info_tag_transform[$token->name]->transform($token, $config, $context); + if ($e) { + $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Tag transform', $original_name); + } + } + + if (isset($definition->info[$token->name])) { + // mostly everything's good, but + // we need to make sure required attributes are in order + if (($token instanceof HTMLPurifier_Token_Start || $token instanceof HTMLPurifier_Token_Empty) && + $definition->info[$token->name]->required_attr && + ($token->name != 'img' || $remove_invalid_img) // ensure config option still works + ) { + $attr_validator->validateToken($token, $config, $context); + $ok = true; + foreach ($definition->info[$token->name]->required_attr as $name) { + if (!isset($token->attr[$name])) { + $ok = false; + break; + } + } + if (!$ok) { + if ($e) { + $e->send( + E_ERROR, + 'Strategy_RemoveForeignElements: Missing required attribute', + $name + ); + } + continue; + } + $token->armor['ValidateAttributes'] = true; + } + + if (isset($hidden_elements[$token->name]) && $token instanceof HTMLPurifier_Token_Start) { + $textify_comments = $token->name; + } elseif ($token->name === $textify_comments && $token instanceof HTMLPurifier_Token_End) { + $textify_comments = false; + } + + } elseif ($escape_invalid_tags) { + // invalid tag, generate HTML representation and insert in + if ($e) { + $e->send(E_WARNING, 'Strategy_RemoveForeignElements: Foreign element to text'); + } + $token = new HTMLPurifier_Token_Text( + $generator->generateFromToken($token) + ); + } else { + // check if we need to destroy all of the tag's children + // CAN BE GENERICIZED + if (isset($hidden_elements[$token->name])) { + if ($token instanceof HTMLPurifier_Token_Start) { + $remove_until = $token->name; + } elseif ($token instanceof HTMLPurifier_Token_Empty) { + // do nothing: we're still looking + } else { + $remove_until = false; + } + if ($e) { + $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign meta element removed'); + } + } else { + if ($e) { + $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign element removed'); + } + } + continue; + } + } elseif ($token instanceof HTMLPurifier_Token_Comment) { + // textify comments in script tags when they are allowed + if ($textify_comments !== false) { + $data = $token->data; + $token = new HTMLPurifier_Token_Text($data); + } elseif ($trusted || $check_comments) { + // always cleanup comments + $trailing_hyphen = false; + if ($e) { + // perform check whether or not there's a trailing hyphen + if (substr($token->data, -1) == '-') { + $trailing_hyphen = true; + } + } + $token->data = rtrim($token->data, '-'); + $found_double_hyphen = false; + while (strpos($token->data, '--') !== false) { + $found_double_hyphen = true; + $token->data = str_replace('--', '-', $token->data); + } + if ($trusted || !empty($comment_lookup[trim($token->data)]) || + ($comment_regexp !== null && preg_match($comment_regexp, trim($token->data)))) { + // OK good + if ($e) { + if ($trailing_hyphen) { + $e->send( + E_NOTICE, + 'Strategy_RemoveForeignElements: Trailing hyphen in comment removed' + ); + } + if ($found_double_hyphen) { + $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Hyphens in comment collapsed'); + } + } + } else { + if ($e) { + $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed'); + } + continue; + } + } else { + // strip comments + if ($e) { + $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed'); + } + continue; + } + } elseif ($token instanceof HTMLPurifier_Token_Text) { + } else { + continue; + } + $result[] = $token; + } + if ($remove_until && $e) { + // we removed tokens until the end, throw error + $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Token removed to end', $remove_until); + } + $context->destroy('CurrentToken'); + return $result; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/ValidateAttributes.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/ValidateAttributes.php new file mode 100644 index 000000000..fbb3d27c8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/ValidateAttributes.php @@ -0,0 +1,45 @@ +<?php + +/** + * Validate all attributes in the tokens. + */ + +class HTMLPurifier_Strategy_ValidateAttributes extends HTMLPurifier_Strategy +{ + + /** + * @param HTMLPurifier_Token[] $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return HTMLPurifier_Token[] + */ + public function execute($tokens, $config, $context) + { + // setup validator + $validator = new HTMLPurifier_AttrValidator(); + + $token = false; + $context->register('CurrentToken', $token); + + foreach ($tokens as $key => $token) { + + // only process tokens that have attributes, + // namely start and empty tags + if (!$token instanceof HTMLPurifier_Token_Start && !$token instanceof HTMLPurifier_Token_Empty) { + continue; + } + + // skip tokens that are armored + if (!empty($token->armor['ValidateAttributes'])) { + continue; + } + + // note that we have no facilities here for removing tokens + $validator->validateToken($token, $config, $context); + } + $context->destroy('CurrentToken'); + return $tokens; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/StringHash.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/StringHash.php new file mode 100644 index 000000000..c07370197 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/StringHash.php @@ -0,0 +1,47 @@ +<?php + +/** + * This is in almost every respect equivalent to an array except + * that it keeps track of which keys were accessed. + * + * @warning For the sake of backwards compatibility with early versions + * of PHP 5, you must not use the $hash[$key] syntax; if you do + * our version of offsetGet is never called. + */ +class HTMLPurifier_StringHash extends ArrayObject +{ + /** + * @type array + */ + protected $accessed = array(); + + /** + * Retrieves a value, and logs the access. + * @param mixed $index + * @return mixed + */ + public function offsetGet($index) + { + $this->accessed[$index] = true; + return parent::offsetGet($index); + } + + /** + * Returns a lookup array of all array indexes that have been accessed. + * @return array in form array($index => true). + */ + public function getAccessed() + { + return $this->accessed; + } + + /** + * Resets the access array. + */ + public function resetAccessed() + { + $this->accessed = array(); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/StringHashParser.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/StringHashParser.php new file mode 100644 index 000000000..7c73f8083 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/StringHashParser.php @@ -0,0 +1,136 @@ +<?php + +/** + * Parses string hash files. File format is as such: + * + * DefaultKeyValue + * KEY: Value + * KEY2: Value2 + * --MULTILINE-KEY-- + * Multiline + * value. + * + * Which would output something similar to: + * + * array( + * 'ID' => 'DefaultKeyValue', + * 'KEY' => 'Value', + * 'KEY2' => 'Value2', + * 'MULTILINE-KEY' => "Multiline\nvalue.\n", + * ) + * + * We use this as an easy to use file-format for configuration schema + * files, but the class itself is usage agnostic. + * + * You can use ---- to forcibly terminate parsing of a single string-hash; + * this marker is used in multi string-hashes to delimit boundaries. + */ +class HTMLPurifier_StringHashParser +{ + + /** + * @type string + */ + public $default = 'ID'; + + /** + * Parses a file that contains a single string-hash. + * @param string $file + * @return array + */ + public function parseFile($file) + { + if (!file_exists($file)) { + return false; + } + $fh = fopen($file, 'r'); + if (!$fh) { + return false; + } + $ret = $this->parseHandle($fh); + fclose($fh); + return $ret; + } + + /** + * Parses a file that contains multiple string-hashes delimited by '----' + * @param string $file + * @return array + */ + public function parseMultiFile($file) + { + if (!file_exists($file)) { + return false; + } + $ret = array(); + $fh = fopen($file, 'r'); + if (!$fh) { + return false; + } + while (!feof($fh)) { + $ret[] = $this->parseHandle($fh); + } + fclose($fh); + return $ret; + } + + /** + * Internal parser that acepts a file handle. + * @note While it's possible to simulate in-memory parsing by using + * custom stream wrappers, if such a use-case arises we should + * factor out the file handle into its own class. + * @param resource $fh File handle with pointer at start of valid string-hash + * block. + * @return array + */ + protected function parseHandle($fh) + { + $state = false; + $single = false; + $ret = array(); + do { + $line = fgets($fh); + if ($line === false) { + break; + } + $line = rtrim($line, "\n\r"); + if (!$state && $line === '') { + continue; + } + if ($line === '----') { + break; + } + if (strncmp('--#', $line, 3) === 0) { + // Comment + continue; + } elseif (strncmp('--', $line, 2) === 0) { + // Multiline declaration + $state = trim($line, '- '); + if (!isset($ret[$state])) { + $ret[$state] = ''; + } + continue; + } elseif (!$state) { + $single = true; + if (strpos($line, ':') !== false) { + // Single-line declaration + list($state, $line) = explode(':', $line, 2); + $line = trim($line); + } else { + // Use default declaration + $state = $this->default; + } + } + if ($single) { + $ret[$state] = $line; + $single = false; + $state = false; + } else { + $ret[$state] .= "$line\n"; + } + } while (!feof($fh)); + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/TagTransform.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/TagTransform.php new file mode 100644 index 000000000..7b8d83343 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/TagTransform.php @@ -0,0 +1,37 @@ +<?php + +/** + * Defines a mutation of an obsolete tag into a valid tag. + */ +abstract class HTMLPurifier_TagTransform +{ + + /** + * Tag name to transform the tag to. + * @type string + */ + public $transform_to; + + /** + * Transforms the obsolete tag into the valid tag. + * @param HTMLPurifier_Token_Tag $tag Tag to be transformed. + * @param HTMLPurifier_Config $config Mandatory HTMLPurifier_Config object + * @param HTMLPurifier_Context $context Mandatory HTMLPurifier_Context object + */ + abstract public function transform($tag, $config, $context); + + /** + * Prepends CSS properties to the style attribute, creating the + * attribute if it doesn't exist. + * @warning Copied over from AttrTransform, be sure to keep in sync + * @param array $attr Attribute array to process (passed by reference) + * @param string $css CSS to prepend + */ + protected function prependCSS(&$attr, $css) + { + $attr['style'] = isset($attr['style']) ? $attr['style'] : ''; + $attr['style'] = $css . $attr['style']; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/TagTransform/Font.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/TagTransform/Font.php new file mode 100644 index 000000000..7853d90bc --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/TagTransform/Font.php @@ -0,0 +1,114 @@ +<?php + +/** + * Transforms FONT tags to the proper form (SPAN with CSS styling) + * + * This transformation takes the three proprietary attributes of FONT and + * transforms them into their corresponding CSS attributes. These are color, + * face, and size. + * + * @note Size is an interesting case because it doesn't map cleanly to CSS. + * Thanks to + * http://style.cleverchimp.com/font_size_intervals/altintervals.html + * for reasonable mappings. + * @warning This doesn't work completely correctly; specifically, this + * TagTransform operates before well-formedness is enforced, so + * the "active formatting elements" algorithm doesn't get applied. + */ +class HTMLPurifier_TagTransform_Font extends HTMLPurifier_TagTransform +{ + /** + * @type string + */ + public $transform_to = 'span'; + + /** + * @type array + */ + protected $_size_lookup = array( + '0' => 'xx-small', + '1' => 'xx-small', + '2' => 'small', + '3' => 'medium', + '4' => 'large', + '5' => 'x-large', + '6' => 'xx-large', + '7' => '300%', + '-1' => 'smaller', + '-2' => '60%', + '+1' => 'larger', + '+2' => '150%', + '+3' => '200%', + '+4' => '300%' + ); + + /** + * @param HTMLPurifier_Token_Tag $tag + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return HTMLPurifier_Token_End|string + */ + public function transform($tag, $config, $context) + { + if ($tag instanceof HTMLPurifier_Token_End) { + $new_tag = clone $tag; + $new_tag->name = $this->transform_to; + return $new_tag; + } + + $attr = $tag->attr; + $prepend_style = ''; + + // handle color transform + if (isset($attr['color'])) { + $prepend_style .= 'color:' . $attr['color'] . ';'; + unset($attr['color']); + } + + // handle face transform + if (isset($attr['face'])) { + $prepend_style .= 'font-family:' . $attr['face'] . ';'; + unset($attr['face']); + } + + // handle size transform + if (isset($attr['size'])) { + // normalize large numbers + if ($attr['size'] !== '') { + if ($attr['size']{0} == '+' || $attr['size']{0} == '-') { + $size = (int)$attr['size']; + if ($size < -2) { + $attr['size'] = '-2'; + } + if ($size > 4) { + $attr['size'] = '+4'; + } + } else { + $size = (int)$attr['size']; + if ($size > 7) { + $attr['size'] = '7'; + } + } + } + if (isset($this->_size_lookup[$attr['size']])) { + $prepend_style .= 'font-size:' . + $this->_size_lookup[$attr['size']] . ';'; + } + unset($attr['size']); + } + + if ($prepend_style) { + $attr['style'] = isset($attr['style']) ? + $prepend_style . $attr['style'] : + $prepend_style; + } + + $new_tag = clone $tag; + $new_tag->name = $this->transform_to; + $new_tag->attr = $attr; + + return $new_tag; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/TagTransform/Simple.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/TagTransform/Simple.php new file mode 100644 index 000000000..71bf10b91 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/TagTransform/Simple.php @@ -0,0 +1,44 @@ +<?php + +/** + * Simple transformation, just change tag name to something else, + * and possibly add some styling. This will cover most of the deprecated + * tag cases. + */ +class HTMLPurifier_TagTransform_Simple extends HTMLPurifier_TagTransform +{ + /** + * @type string + */ + protected $style; + + /** + * @param string $transform_to Tag name to transform to. + * @param string $style CSS style to add to the tag + */ + public function __construct($transform_to, $style = null) + { + $this->transform_to = $transform_to; + $this->style = $style; + } + + /** + * @param HTMLPurifier_Token_Tag $tag + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function transform($tag, $config, $context) + { + $new_tag = clone $tag; + $new_tag->name = $this->transform_to; + if (!is_null($this->style) && + ($new_tag instanceof HTMLPurifier_Token_Start || $new_tag instanceof HTMLPurifier_Token_Empty) + ) { + $this->prependCSS($new_tag->attr, $this->style); + } + return $new_tag; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token.php new file mode 100644 index 000000000..84d3619a3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token.php @@ -0,0 +1,100 @@ +<?php + +/** + * Abstract base token class that all others inherit from. + */ +abstract class HTMLPurifier_Token +{ + /** + * Line number node was on in source document. Null if unknown. + * @type int + */ + public $line; + + /** + * Column of line node was on in source document. Null if unknown. + * @type int + */ + public $col; + + /** + * Lookup array of processing that this token is exempt from. + * Currently, valid values are "ValidateAttributes" and + * "MakeWellFormed_TagClosedError" + * @type array + */ + public $armor = array(); + + /** + * Used during MakeWellFormed. See Note [Injector skips] + * @type + */ + public $skip; + + /** + * @type + */ + public $rewind; + + /** + * @type + */ + public $carryover; + + /** + * @param string $n + * @return null|string + */ + public function __get($n) + { + if ($n === 'type') { + trigger_error('Deprecated type property called; use instanceof', E_USER_NOTICE); + switch (get_class($this)) { + case 'HTMLPurifier_Token_Start': + return 'start'; + case 'HTMLPurifier_Token_Empty': + return 'empty'; + case 'HTMLPurifier_Token_End': + return 'end'; + case 'HTMLPurifier_Token_Text': + return 'text'; + case 'HTMLPurifier_Token_Comment': + return 'comment'; + default: + return null; + } + } + } + + /** + * Sets the position of the token in the source document. + * @param int $l + * @param int $c + */ + public function position($l = null, $c = null) + { + $this->line = $l; + $this->col = $c; + } + + /** + * Convenience function for DirectLex settings line/col position. + * @param int $l + * @param int $c + */ + public function rawPosition($l, $c) + { + if ($c === -1) { + $l++; + } + $this->line = $l; + $this->col = $c; + } + + /** + * Converts a token into its corresponding node. + */ + abstract public function toNode(); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token/Comment.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token/Comment.php new file mode 100644 index 000000000..23453c705 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token/Comment.php @@ -0,0 +1,38 @@ +<?php + +/** + * Concrete comment token class. Generally will be ignored. + */ +class HTMLPurifier_Token_Comment extends HTMLPurifier_Token +{ + /** + * Character data within comment. + * @type string + */ + public $data; + + /** + * @type bool + */ + public $is_whitespace = true; + + /** + * Transparent constructor. + * + * @param string $data String comment data. + * @param int $line + * @param int $col + */ + public function __construct($data, $line = null, $col = null) + { + $this->data = $data; + $this->line = $line; + $this->col = $col; + } + + public function toNode() { + return new HTMLPurifier_Node_Comment($this->data, $this->line, $this->col); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token/Empty.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token/Empty.php new file mode 100644 index 000000000..78a95f555 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token/Empty.php @@ -0,0 +1,15 @@ +<?php + +/** + * Concrete empty token class. + */ +class HTMLPurifier_Token_Empty extends HTMLPurifier_Token_Tag +{ + public function toNode() { + $n = parent::toNode(); + $n->empty = true; + return $n; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token/End.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token/End.php new file mode 100644 index 000000000..59b38fdc5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token/End.php @@ -0,0 +1,24 @@ +<?php + +/** + * Concrete end token class. + * + * @warning This class accepts attributes even though end tags cannot. This + * is for optimization reasons, as under normal circumstances, the Lexers + * do not pass attributes. + */ +class HTMLPurifier_Token_End extends HTMLPurifier_Token_Tag +{ + /** + * Token that started this node. + * Added by MakeWellFormed. Please do not edit this! + * @type HTMLPurifier_Token + */ + public $start; + + public function toNode() { + throw new Exception("HTMLPurifier_Token_End->toNode not supported!"); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token/Start.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token/Start.php new file mode 100644 index 000000000..019f317ad --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token/Start.php @@ -0,0 +1,10 @@ +<?php + +/** + * Concrete start token class. + */ +class HTMLPurifier_Token_Start extends HTMLPurifier_Token_Tag +{ +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token/Tag.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token/Tag.php new file mode 100644 index 000000000..d643fa64e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token/Tag.php @@ -0,0 +1,68 @@ +<?php + +/** + * Abstract class of a tag token (start, end or empty), and its behavior. + */ +abstract class HTMLPurifier_Token_Tag extends HTMLPurifier_Token +{ + /** + * Static bool marker that indicates the class is a tag. + * + * This allows us to check objects with <tt>!empty($obj->is_tag)</tt> + * without having to use a function call <tt>is_a()</tt>. + * @type bool + */ + public $is_tag = true; + + /** + * The lower-case name of the tag, like 'a', 'b' or 'blockquote'. + * + * @note Strictly speaking, XML tags are case sensitive, so we shouldn't + * be lower-casing them, but these tokens cater to HTML tags, which are + * insensitive. + * @type string + */ + public $name; + + /** + * Associative array of the tag's attributes. + * @type array + */ + public $attr = array(); + + /** + * Non-overloaded constructor, which lower-cases passed tag name. + * + * @param string $name String name. + * @param array $attr Associative array of attributes. + * @param int $line + * @param int $col + * @param array $armor + */ + public function __construct($name, $attr = array(), $line = null, $col = null, $armor = array()) + { + $this->name = ctype_lower($name) ? $name : strtolower($name); + foreach ($attr as $key => $value) { + // normalization only necessary when key is not lowercase + if (!ctype_lower($key)) { + $new_key = strtolower($key); + if (!isset($attr[$new_key])) { + $attr[$new_key] = $attr[$key]; + } + if ($new_key !== $key) { + unset($attr[$key]); + } + } + } + $this->attr = $attr; + $this->line = $line; + $this->col = $col; + $this->armor = $armor; + } + + public function toNode() { + return new HTMLPurifier_Node_Element($this->name, $this->attr, $this->line, $this->col, $this->armor); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token/Text.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token/Text.php new file mode 100644 index 000000000..f26a1c211 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Token/Text.php @@ -0,0 +1,53 @@ +<?php + +/** + * Concrete text token class. + * + * Text tokens comprise of regular parsed character data (PCDATA) and raw + * character data (from the CDATA sections). Internally, their + * data is parsed with all entities expanded. Surprisingly, the text token + * does have a "tag name" called #PCDATA, which is how the DTD represents it + * in permissible child nodes. + */ +class HTMLPurifier_Token_Text extends HTMLPurifier_Token +{ + + /** + * @type string + */ + public $name = '#PCDATA'; + /**< PCDATA tag name compatible with DTD. */ + + /** + * @type string + */ + public $data; + /**< Parsed character data of text. */ + + /** + * @type bool + */ + public $is_whitespace; + + /**< Bool indicating if node is whitespace. */ + + /** + * Constructor, accepts data and determines if it is whitespace. + * @param string $data String parsed character data. + * @param int $line + * @param int $col + */ + public function __construct($data, $line = null, $col = null) + { + $this->data = $data; + $this->is_whitespace = ctype_space($data); + $this->line = $line; + $this->col = $col; + } + + public function toNode() { + return new HTMLPurifier_Node_Text($this->data, $this->is_whitespace, $this->line, $this->col); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/TokenFactory.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/TokenFactory.php new file mode 100644 index 000000000..dea2446b9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/TokenFactory.php @@ -0,0 +1,118 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URI.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URI.php new file mode 100644 index 000000000..9c5be39d1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URI.php @@ -0,0 +1,316 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIDefinition.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIDefinition.php new file mode 100644 index 000000000..e0bd8bcca --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIDefinition.php @@ -0,0 +1,112 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter.php new file mode 100644 index 000000000..09724e9f4 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter.php @@ -0,0 +1,74 @@ +<?php + +/** + * Chainable filters for custom URI processing. + * + * These filters can perform custom actions on a URI filter object, + * including transformation or blacklisting. A filter named Foo + * must have a corresponding configuration directive %URI.Foo, + * unless always_load is specified to be true. + * + * The following contexts may be available while URIFilters are being + * processed: + * + * - EmbeddedURI: true if URI is an embedded resource that will + * be loaded automatically on page load + * - CurrentToken: a reference to the token that is currently + * being processed + * - CurrentAttr: the name of the attribute that is currently being + * processed + * - CurrentCSSProperty: the name of the CSS property that is + * currently being processed (if applicable) + * + * @warning This filter is called before scheme object validation occurs. + * Make sure, if you require a specific scheme object, you + * you check that it exists. This allows filters to convert + * proprietary URI schemes into regular ones. + */ +abstract class HTMLPurifier_URIFilter +{ + + /** + * Unique identifier of filter. + * @type string + */ + public $name; + + /** + * True if this filter should be run after scheme validation. + * @type bool + */ + public $post = false; + + /** + * True if this filter should always be loaded. + * This permits a filter to be named Foo without the corresponding + * %URI.Foo directive existing. + * @type bool + */ + public $always_load = false; + + /** + * Performs initialization for the filter. If the filter returns + * false, this means that it shouldn't be considered active. + * @param HTMLPurifier_Config $config + * @return bool + */ + public function prepare($config) + { + return true; + } + + /** + * Filter a URI object + * @param HTMLPurifier_URI $uri Reference to URI object variable + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool Whether or not to continue processing: false indicates + * URL is no good, true indicates continue processing. Note that + * all changes are committed directly on the URI object + */ + abstract public function filter(&$uri, $config, $context); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/DisableExternal.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/DisableExternal.php new file mode 100644 index 000000000..ced1b1376 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/DisableExternal.php @@ -0,0 +1,54 @@ +<?php + +class HTMLPurifier_URIFilter_DisableExternal extends HTMLPurifier_URIFilter +{ + /** + * @type string + */ + public $name = 'DisableExternal'; + + /** + * @type array + */ + protected $ourHostParts = false; + + /** + * @param HTMLPurifier_Config $config + * @return void + */ + public function prepare($config) + { + $our_host = $config->getDefinition('URI')->host; + if ($our_host !== null) { + $this->ourHostParts = array_reverse(explode('.', $our_host)); + } + } + + /** + * @param HTMLPurifier_URI $uri Reference + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + if (is_null($uri->host)) { + return true; + } + if ($this->ourHostParts === false) { + return false; + } + $host_parts = array_reverse(explode('.', $uri->host)); + foreach ($this->ourHostParts as $i => $x) { + if (!isset($host_parts[$i])) { + return false; + } + if ($host_parts[$i] != $this->ourHostParts[$i]) { + return false; + } + } + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/DisableExternalResources.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/DisableExternalResources.php new file mode 100644 index 000000000..c6562169e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/DisableExternalResources.php @@ -0,0 +1,25 @@ +<?php + +class HTMLPurifier_URIFilter_DisableExternalResources extends HTMLPurifier_URIFilter_DisableExternal +{ + /** + * @type string + */ + public $name = 'DisableExternalResources'; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + if (!$context->get('EmbeddedURI', true)) { + return true; + } + return parent::filter($uri, $config, $context); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/DisableResources.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/DisableResources.php new file mode 100644 index 000000000..d5c412c44 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/DisableResources.php @@ -0,0 +1,22 @@ +<?php + +class HTMLPurifier_URIFilter_DisableResources extends HTMLPurifier_URIFilter +{ + /** + * @type string + */ + public $name = 'DisableResources'; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + return !$context->get('EmbeddedURI', true); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/HostBlacklist.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/HostBlacklist.php new file mode 100644 index 000000000..a6645c17e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/HostBlacklist.php @@ -0,0 +1,46 @@ +<?php + +// It's not clear to me whether or not Punycode means that hostnames +// do not have canonical forms anymore. As far as I can tell, it's +// not a problem (punycoding should be identity when no Unicode +// points are involved), but I'm not 100% sure +class HTMLPurifier_URIFilter_HostBlacklist extends HTMLPurifier_URIFilter +{ + /** + * @type string + */ + public $name = 'HostBlacklist'; + + /** + * @type array + */ + protected $blacklist = array(); + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function prepare($config) + { + $this->blacklist = $config->get('URI.HostBlacklist'); + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + foreach ($this->blacklist as $blacklisted_host_fragment) { + if (strpos($uri->host, $blacklisted_host_fragment) !== false) { + return false; + } + } + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/MakeAbsolute.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/MakeAbsolute.php new file mode 100644 index 000000000..c507bbff8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/MakeAbsolute.php @@ -0,0 +1,158 @@ +<?php + +// does not support network paths + +class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter +{ + /** + * @type string + */ + public $name = 'MakeAbsolute'; + + /** + * @type + */ + protected $base; + + /** + * @type array + */ + protected $basePathStack = array(); + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function prepare($config) + { + $def = $config->getDefinition('URI'); + $this->base = $def->base; + if (is_null($this->base)) { + trigger_error( + 'URI.MakeAbsolute is being ignored due to lack of ' . + 'value for URI.Base configuration', + E_USER_WARNING + ); + return false; + } + $this->base->fragment = null; // fragment is invalid for base URI + $stack = explode('/', $this->base->path); + array_pop($stack); // discard last segment + $stack = $this->_collapseStack($stack); // do pre-parsing + $this->basePathStack = $stack; + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + if (is_null($this->base)) { + return true; + } // abort early + if ($uri->path === '' && is_null($uri->scheme) && + is_null($uri->host) && is_null($uri->query) && is_null($uri->fragment)) { + // reference to current document + $uri = clone $this->base; + return true; + } + if (!is_null($uri->scheme)) { + // absolute URI already: don't change + if (!is_null($uri->host)) { + return true; + } + $scheme_obj = $uri->getSchemeObj($config, $context); + if (!$scheme_obj) { + // scheme not recognized + return false; + } + if (!$scheme_obj->hierarchical) { + // non-hierarchal URI with explicit scheme, don't change + return true; + } + // special case: had a scheme but always is hierarchical and had no authority + } + if (!is_null($uri->host)) { + // network path, don't bother + return true; + } + if ($uri->path === '') { + $uri->path = $this->base->path; + } elseif ($uri->path[0] !== '/') { + // relative path, needs more complicated processing + $stack = explode('/', $uri->path); + $new_stack = array_merge($this->basePathStack, $stack); + if ($new_stack[0] !== '' && !is_null($this->base->host)) { + array_unshift($new_stack, ''); + } + $new_stack = $this->_collapseStack($new_stack); + $uri->path = implode('/', $new_stack); + } else { + // absolute path, but still we should collapse + $uri->path = implode('/', $this->_collapseStack(explode('/', $uri->path))); + } + // re-combine + $uri->scheme = $this->base->scheme; + if (is_null($uri->userinfo)) { + $uri->userinfo = $this->base->userinfo; + } + if (is_null($uri->host)) { + $uri->host = $this->base->host; + } + if (is_null($uri->port)) { + $uri->port = $this->base->port; + } + return true; + } + + /** + * Resolve dots and double-dots in a path stack + * @param array $stack + * @return array + */ + private function _collapseStack($stack) + { + $result = array(); + $is_folder = false; + for ($i = 0; isset($stack[$i]); $i++) { + $is_folder = false; + // absorb an internally duplicated slash + if ($stack[$i] == '' && $i && isset($stack[$i + 1])) { + continue; + } + if ($stack[$i] == '..') { + if (!empty($result)) { + $segment = array_pop($result); + if ($segment === '' && empty($result)) { + // error case: attempted to back out too far: + // restore the leading slash + $result[] = ''; + } elseif ($segment === '..') { + $result[] = '..'; // cannot remove .. with .. + } + } else { + // relative path, preserve the double-dots + $result[] = '..'; + } + $is_folder = true; + continue; + } + if ($stack[$i] == '.') { + // silently absorb + $is_folder = true; + continue; + } + $result[] = $stack[$i]; + } + if ($is_folder) { + $result[] = ''; + } + return $result; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/Munge.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/Munge.php new file mode 100644 index 000000000..6e03315a1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/Munge.php @@ -0,0 +1,115 @@ +<?php + +class HTMLPurifier_URIFilter_Munge extends HTMLPurifier_URIFilter +{ + /** + * @type string + */ + public $name = 'Munge'; + + /** + * @type bool + */ + public $post = true; + + /** + * @type string + */ + private $target; + + /** + * @type HTMLPurifier_URIParser + */ + private $parser; + + /** + * @type bool + */ + private $doEmbed; + + /** + * @type string + */ + private $secretKey; + + /** + * @type array + */ + protected $replace = array(); + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function prepare($config) + { + $this->target = $config->get('URI.' . $this->name); + $this->parser = new HTMLPurifier_URIParser(); + $this->doEmbed = $config->get('URI.MungeResources'); + $this->secretKey = $config->get('URI.MungeSecretKey'); + if ($this->secretKey && !function_exists('hash_hmac')) { + throw new Exception("Cannot use %URI.MungeSecretKey without hash_hmac support."); + } + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + if ($context->get('EmbeddedURI', true) && !$this->doEmbed) { + return true; + } + + $scheme_obj = $uri->getSchemeObj($config, $context); + if (!$scheme_obj) { + return true; + } // ignore unknown schemes, maybe another postfilter did it + if (!$scheme_obj->browsable) { + return true; + } // ignore non-browseable schemes, since we can't munge those in a reasonable way + if ($uri->isBenign($config, $context)) { + return true; + } // don't redirect if a benign URL + + $this->makeReplace($uri, $config, $context); + $this->replace = array_map('rawurlencode', $this->replace); + + $new_uri = strtr($this->target, $this->replace); + $new_uri = $this->parser->parse($new_uri); + // don't redirect if the target host is the same as the + // starting host + if ($uri->host === $new_uri->host) { + return true; + } + $uri = $new_uri; // overwrite + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + */ + protected function makeReplace($uri, $config, $context) + { + $string = $uri->toString(); + // always available + $this->replace['%s'] = $string; + $this->replace['%r'] = $context->get('EmbeddedURI', true); + $token = $context->get('CurrentToken', true); + $this->replace['%n'] = $token ? $token->name : null; + $this->replace['%m'] = $context->get('CurrentAttr', true); + $this->replace['%p'] = $context->get('CurrentCSSProperty', true); + // not always available + if ($this->secretKey) { + $this->replace['%t'] = hash_hmac("sha256", $string, $this->secretKey); + } + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/SafeIframe.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/SafeIframe.php new file mode 100644 index 000000000..f609c47a3 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIFilter/SafeIframe.php @@ -0,0 +1,68 @@ +<?php + +/** + * Implements safety checks for safe iframes. + * + * @warning This filter is *critical* for ensuring that %HTML.SafeIframe + * works safely. + */ +class HTMLPurifier_URIFilter_SafeIframe extends HTMLPurifier_URIFilter +{ + /** + * @type string + */ + public $name = 'SafeIframe'; + + /** + * @type bool + */ + public $always_load = true; + + /** + * @type string + */ + protected $regexp = null; + + // XXX: The not so good bit about how this is all set up now is we + // can't check HTML.SafeIframe in the 'prepare' step: we have to + // defer till the actual filtering. + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function prepare($config) + { + $this->regexp = $config->get('URI.SafeIframeRegexp'); + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + // check if filter not applicable + if (!$config->get('HTML.SafeIframe')) { + return true; + } + // check if the filter should actually trigger + if (!$context->get('EmbeddedURI', true)) { + return true; + } + $token = $context->get('CurrentToken', true); + if (!($token && $token->name == 'iframe')) { + return true; + } + // check if we actually have some whitelists enabled + if ($this->regexp === null) { + return false; + } + // actually check the whitelists + return preg_match($this->regexp, $uri->toString()); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIParser.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIParser.php new file mode 100644 index 000000000..0e7381a07 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIParser.php @@ -0,0 +1,71 @@ +<?php + +/** + * Parses a URI into the components and fragment identifier as specified + * by RFC 3986. + */ +class HTMLPurifier_URIParser +{ + + /** + * Instance of HTMLPurifier_PercentEncoder to do normalization with. + */ + protected $percentEncoder; + + public function __construct() + { + $this->percentEncoder = new HTMLPurifier_PercentEncoder(); + } + + /** + * Parses a URI. + * @param $uri string URI to parse + * @return HTMLPurifier_URI representation of URI. This representation has + * not been validated yet and may not conform to RFC. + */ + public function parse($uri) + { + $uri = $this->percentEncoder->normalize($uri); + + // Regexp is as per Appendix B. + // Note that ["<>] are an addition to the RFC's recommended + // characters, because they represent external delimeters. + $r_URI = '!'. + '(([a-zA-Z0-9\.\+\-]+):)?'. // 2. Scheme + '(//([^/?#"<>]*))?'. // 4. Authority + '([^?#"<>]*)'. // 5. Path + '(\?([^#"<>]*))?'. // 7. Query + '(#([^"<>]*))?'. // 8. Fragment + '!'; + + $matches = array(); + $result = preg_match($r_URI, $uri, $matches); + + if (!$result) return false; // *really* invalid URI + + // seperate out parts + $scheme = !empty($matches[1]) ? $matches[2] : null; + $authority = !empty($matches[3]) ? $matches[4] : null; + $path = $matches[5]; // always present, can be empty + $query = !empty($matches[6]) ? $matches[7] : null; + $fragment = !empty($matches[8]) ? $matches[9] : null; + + // further parse authority + if ($authority !== null) { + $r_authority = "/^((.+?)@)?(\[[^\]]+\]|[^:]*)(:(\d*))?/"; + $matches = array(); + preg_match($r_authority, $authority, $matches); + $userinfo = !empty($matches[1]) ? $matches[2] : null; + $host = !empty($matches[3]) ? $matches[3] : ''; + $port = !empty($matches[4]) ? (int) $matches[5] : null; + } else { + $port = $host = $userinfo = null; + } + + return new HTMLPurifier_URI( + $scheme, $userinfo, $host, $port, $path, $query, $fragment); + } + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme.php new file mode 100644 index 000000000..fe9e82cf2 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme.php @@ -0,0 +1,102 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/data.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/data.php new file mode 100644 index 000000000..41c49d553 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/data.php @@ -0,0 +1,136 @@ +<?php + +/** + * Implements data: URI for base64 encoded images supported by GD. + */ +class HTMLPurifier_URIScheme_data extends HTMLPurifier_URIScheme +{ + /** + * @type bool + */ + public $browsable = true; + + /** + * @type array + */ + public $allowed_types = array( + // you better write validation code for other types if you + // decide to allow them + 'image/jpeg' => true, + 'image/gif' => true, + 'image/png' => true, + ); + // this is actually irrelevant since we only write out the path + // component + /** + * @type bool + */ + public $may_omit_host = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $result = explode(',', $uri->path, 2); + $is_base64 = false; + $charset = null; + $content_type = null; + if (count($result) == 2) { + list($metadata, $data) = $result; + // do some legwork on the metadata + $metas = explode(';', $metadata); + while (!empty($metas)) { + $cur = array_shift($metas); + if ($cur == 'base64') { + $is_base64 = true; + break; + } + if (substr($cur, 0, 8) == 'charset=') { + // doesn't match if there are arbitrary spaces, but + // whatever dude + if ($charset !== null) { + continue; + } // garbage + $charset = substr($cur, 8); // not used + } else { + if ($content_type !== null) { + continue; + } // garbage + $content_type = $cur; + } + } + } else { + $data = $result[0]; + } + if ($content_type !== null && empty($this->allowed_types[$content_type])) { + return false; + } + if ($charset !== null) { + // error; we don't allow plaintext stuff + $charset = null; + } + $data = rawurldecode($data); + if ($is_base64) { + $raw_data = base64_decode($data); + } else { + $raw_data = $data; + } + if ( strlen($raw_data) < 12 ) { + // error; exif_imagetype throws exception with small files, + // and this likely indicates a corrupt URI/failed parse anyway + return false; + } + // XXX probably want to refactor this into a general mechanism + // for filtering arbitrary content types + if (function_exists('sys_get_temp_dir')) { + $file = tempnam(sys_get_temp_dir(), ""); + } else { + $file = tempnam("/tmp", ""); + } + file_put_contents($file, $raw_data); + if (function_exists('exif_imagetype')) { + $image_code = exif_imagetype($file); + unlink($file); + } elseif (function_exists('getimagesize')) { + set_error_handler(array($this, 'muteErrorHandler')); + $info = getimagesize($file); + restore_error_handler(); + unlink($file); + if ($info == false) { + return false; + } + $image_code = $info[2]; + } else { + trigger_error("could not find exif_imagetype or getimagesize functions", E_USER_ERROR); + } + $real_content_type = image_type_to_mime_type($image_code); + if ($real_content_type != $content_type) { + // we're nice guys; if the content type is something else we + // support, change it over + if (empty($this->allowed_types[$real_content_type])) { + return false; + } + $content_type = $real_content_type; + } + // ok, it's kosher, rewrite what we need + $uri->userinfo = null; + $uri->host = null; + $uri->port = null; + $uri->fragment = null; + $uri->query = null; + $uri->path = "$content_type;base64," . base64_encode($raw_data); + return true; + } + + /** + * @param int $errno + * @param string $errstr + */ + public function muteErrorHandler($errno, $errstr) + { + } +} diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/file.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/file.php new file mode 100644 index 000000000..215be4ba8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/file.php @@ -0,0 +1,44 @@ +<?php + +/** + * Validates file as defined by RFC 1630 and RFC 1738. + */ +class HTMLPurifier_URIScheme_file extends HTMLPurifier_URIScheme +{ + /** + * Generally file:// URLs are not accessible from most + * machines, so placing them as an img src is incorrect. + * @type bool + */ + public $browsable = false; + + /** + * Basically the *only* URI scheme for which this is true, since + * accessing files on the local machine is very common. In fact, + * browsers on some operating systems don't understand the + * authority, though I hear it is used on Windows to refer to + * network shares. + * @type bool + */ + public $may_omit_host = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + // Authentication method is not supported + $uri->userinfo = null; + // file:// makes no provisions for accessing the resource + $uri->port = null; + // While it seems to work on Firefox, the querystring has + // no possible effect and is thus stripped. + $uri->query = null; + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/ftp.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/ftp.php new file mode 100644 index 000000000..1eb43ee5c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/ftp.php @@ -0,0 +1,58 @@ +<?php + +/** + * Validates ftp (File Transfer Protocol) URIs as defined by generic RFC 1738. + */ +class HTMLPurifier_URIScheme_ftp extends HTMLPurifier_URIScheme +{ + /** + * @type int + */ + public $default_port = 21; + + /** + * @type bool + */ + public $browsable = true; // usually + + /** + * @type bool + */ + public $hierarchical = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $uri->query = null; + + // typecode check + $semicolon_pos = strrpos($uri->path, ';'); // reverse + if ($semicolon_pos !== false) { + $type = substr($uri->path, $semicolon_pos + 1); // no semicolon + $uri->path = substr($uri->path, 0, $semicolon_pos); + $type_ret = ''; + if (strpos($type, '=') !== false) { + // figure out whether or not the declaration is correct + list($key, $typecode) = explode('=', $type, 2); + if ($key !== 'type') { + // invalid key, tack it back on encoded + $uri->path .= '%3B' . $type; + } elseif ($typecode === 'a' || $typecode === 'i' || $typecode === 'd') { + $type_ret = ";type=$typecode"; + } + } else { + $uri->path .= '%3B' . $type; + } + $uri->path = str_replace(';', '%3B', $uri->path); + $uri->path .= $type_ret; + } + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/http.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/http.php new file mode 100644 index 000000000..ce69ec438 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/http.php @@ -0,0 +1,36 @@ +<?php + +/** + * Validates http (HyperText Transfer Protocol) as defined by RFC 2616 + */ +class HTMLPurifier_URIScheme_http extends HTMLPurifier_URIScheme +{ + /** + * @type int + */ + public $default_port = 80; + + /** + * @type bool + */ + public $browsable = true; + + /** + * @type bool + */ + public $hierarchical = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $uri->userinfo = null; + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/https.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/https.php new file mode 100644 index 000000000..0e96882db --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/https.php @@ -0,0 +1,18 @@ +<?php + +/** + * Validates https (Secure HTTP) according to http scheme. + */ +class HTMLPurifier_URIScheme_https extends HTMLPurifier_URIScheme_http +{ + /** + * @type int + */ + public $default_port = 443; + /** + * @type bool + */ + public $secure = true; +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/mailto.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/mailto.php new file mode 100644 index 000000000..c3a6b602a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/mailto.php @@ -0,0 +1,40 @@ +<?php + +// VERY RELAXED! Shouldn't cause problems, not even Firefox checks if the +// email is valid, but be careful! + +/** + * Validates mailto (for E-mail) according to RFC 2368 + * @todo Validate the email address + * @todo Filter allowed query parameters + */ + +class HTMLPurifier_URIScheme_mailto extends HTMLPurifier_URIScheme +{ + /** + * @type bool + */ + public $browsable = false; + + /** + * @type bool + */ + public $may_omit_host = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $uri->userinfo = null; + $uri->host = null; + $uri->port = null; + // we need to validate path against RFC 2368's addr-spec + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/news.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/news.php new file mode 100644 index 000000000..7490927d6 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/news.php @@ -0,0 +1,35 @@ +<?php + +/** + * Validates news (Usenet) as defined by generic RFC 1738 + */ +class HTMLPurifier_URIScheme_news extends HTMLPurifier_URIScheme +{ + /** + * @type bool + */ + public $browsable = false; + + /** + * @type bool + */ + public $may_omit_host = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $uri->userinfo = null; + $uri->host = null; + $uri->port = null; + $uri->query = null; + // typecode check needed on path + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/nntp.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/nntp.php new file mode 100644 index 000000000..f211d715e --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/nntp.php @@ -0,0 +1,32 @@ +<?php + +/** + * Validates nntp (Network News Transfer Protocol) as defined by generic RFC 1738 + */ +class HTMLPurifier_URIScheme_nntp extends HTMLPurifier_URIScheme +{ + /** + * @type int + */ + public $default_port = 119; + + /** + * @type bool + */ + public $browsable = false; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $uri->userinfo = null; + $uri->query = null; + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/tel.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/tel.php new file mode 100644 index 000000000..8cd193352 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URIScheme/tel.php @@ -0,0 +1,46 @@ +<?php + +/** + * Validates tel (for phone numbers). + * + * The relevant specifications for this protocol are RFC 3966 and RFC 5341, + * but this class takes a much simpler approach: we normalize phone + * numbers so that they only include (possibly) a leading plus, + * and then any number of digits and x'es. + */ + +class HTMLPurifier_URIScheme_tel extends HTMLPurifier_URIScheme +{ + /** + * @type bool + */ + public $browsable = false; + + /** + * @type bool + */ + public $may_omit_host = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $uri->userinfo = null; + $uri->host = null; + $uri->port = null; + + // Delete all non-numeric characters, non-x characters + // from phone number, EXCEPT for a leading plus sign. + $uri->path = preg_replace('/(?!^\+)[^\dx]/', '', + // Normalize e(x)tension to lower-case + str_replace('X', 'x', $uri->path)); + + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URISchemeRegistry.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URISchemeRegistry.php new file mode 100644 index 000000000..4ac8a0b76 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/URISchemeRegistry.php @@ -0,0 +1,81 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/UnitConverter.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/UnitConverter.php new file mode 100644 index 000000000..166f3bf30 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/UnitConverter.php @@ -0,0 +1,307 @@ +<?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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/VarParser.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/VarParser.php new file mode 100644 index 000000000..50cba6910 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/VarParser.php @@ -0,0 +1,198 @@ +<?php + +/** + * Parses string representations into their corresponding native PHP + * variable type. The base implementation does a simple type-check. + */ +class HTMLPurifier_VarParser +{ + + const STRING = 1; + const ISTRING = 2; + const TEXT = 3; + const ITEXT = 4; + const INT = 5; + const FLOAT = 6; + const BOOL = 7; + const LOOKUP = 8; + const ALIST = 9; + const HASH = 10; + const 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::STRING, + 'istring' => self::ISTRING, + 'text' => self::TEXT, + 'itext' => self::ITEXT, + 'int' => self::INT, + 'float' => self::FLOAT, + 'bool' => self::BOOL, + 'lookup' => self::LOOKUP, + 'list' => self::ALIST, + 'hash' => self::HASH, + 'mixed' => self::MIXED + ); + + /** + * Lookup table of types that are string, and can have aliases or + * allowed value lists. + */ + public static $stringTypes = array( + self::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::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::INT): + if (!is_int($var)) { + break; + } + return $var; + case (self::FLOAT): + if (!is_float($var)) { + break; + } + return $var; + case (self::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::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 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/VarParser/Flexible.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/VarParser/Flexible.php new file mode 100644 index 000000000..b15016c5b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/VarParser/Flexible.php @@ -0,0 +1,130 @@ +<?php + +/** + * Performs safe variable parsing based on types which can be used by + * users. This may not be able to represent all possible data inputs, + * however. + */ +class HTMLPurifier_VarParser_Flexible extends HTMLPurifier_VarParser +{ + /** + * @param mixed $var + * @param int $type + * @param bool $allow_null + * @return array|bool|float|int|mixed|null|string + * @throws HTMLPurifier_VarParserException + */ + protected function parseImplementation($var, $type, $allow_null) + { + if ($allow_null && $var === null) { + return null; + } + switch ($type) { + // Note: if code "breaks" from the switch, it triggers a generic + // exception to be thrown. Specific errors can be specifically + // done here. + case self::MIXED: + case self::ISTRING: + case self::STRING: + case self::TEXT: + case self::ITEXT: + return $var; + case self::INT: + if (is_string($var) && ctype_digit($var)) { + $var = (int)$var; + } + return $var; + case self::FLOAT: + if ((is_string($var) && is_numeric($var)) || is_int($var)) { + $var = (float)$var; + } + return $var; + case self::BOOL: + if (is_int($var) && ($var === 0 || $var === 1)) { + $var = (bool)$var; + } elseif (is_string($var)) { + if ($var == 'on' || $var == 'true' || $var == '1') { + $var = true; + } elseif ($var == 'off' || $var == 'false' || $var == '0') { + $var = false; + } else { + throw new HTMLPurifier_VarParserException("Unrecognized value '$var' for $type"); + } + } + return $var; + case self::ALIST: + case self::HASH: + case self::LOOKUP: + if (is_string($var)) { + // special case: technically, this is an array with + // a single empty string item, but having an empty + // array is more intuitive + if ($var == '') { + return array(); + } + if (strpos($var, "\n") === false && strpos($var, "\r") === false) { + // simplistic string to array method that only works + // for simple lists of tag names or alphanumeric characters + $var = explode(',', $var); + } else { + $var = preg_split('/(,|[\n\r]+)/', $var); + } + // remove spaces + foreach ($var as $i => $j) { + $var[$i] = trim($j); + } + if ($type === self::HASH) { + // key:value,key2:value2 + $nvar = array(); + foreach ($var as $keypair) { + $c = explode(':', $keypair, 2); + if (!isset($c[1])) { + continue; + } + $nvar[trim($c[0])] = trim($c[1]); + } + $var = $nvar; + } + } + if (!is_array($var)) { + break; + } + $keys = array_keys($var); + if ($keys === array_keys($keys)) { + if ($type == self::ALIST) { + return $var; + } elseif ($type == self::LOOKUP) { + $new = array(); + foreach ($var as $key) { + $new[$key] = true; + } + return $new; + } else { + break; + } + } + if ($type === self::ALIST) { + trigger_error("Array list did not have consecutive integer indexes", E_USER_WARNING); + return array_values($var); + } + if ($type === self::LOOKUP) { + foreach ($var as $key => $value) { + if ($value !== true) { + trigger_error( + "Lookup array has non-true value at key '$key'; " . + "maybe your input array was not indexed numerically", + E_USER_WARNING + ); + } + $var[$key] = true; + } + } + return $var; + default: + $this->errorInconsistent(__CLASS__, $type); + } + $this->errorGeneric($var, $type); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/VarParser/Native.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/VarParser/Native.php new file mode 100644 index 000000000..f11c318ef --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/VarParser/Native.php @@ -0,0 +1,38 @@ +<?php + +/** + * This variable parser uses PHP's internal code engine. Because it does + * this, it can represent all inputs; however, it is dangerous and cannot + * be used by users. + */ +class HTMLPurifier_VarParser_Native extends HTMLPurifier_VarParser +{ + + /** + * @param mixed $var + * @param int $type + * @param bool $allow_null + * @return null|string + */ + protected function parseImplementation($var, $type, $allow_null) + { + return $this->evalExpression($var); + } + + /** + * @param string $expr + * @return mixed + * @throws HTMLPurifier_VarParserException + */ + protected function evalExpression($expr) + { + $var = null; + $result = eval("\$var = $expr;"); + if ($result === false) { + throw new HTMLPurifier_VarParserException("Fatal error in evaluated code"); + } + return $var; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/VarParserException.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/VarParserException.php new file mode 100644 index 000000000..5df341495 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/VarParserException.php @@ -0,0 +1,11 @@ +<?php + +/** + * Exception type for HTMLPurifier_VarParser + */ +class HTMLPurifier_VarParserException extends HTMLPurifier_Exception +{ + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Zipper.php b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Zipper.php new file mode 100644 index 000000000..6e21ea070 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Zipper.php @@ -0,0 +1,157 @@ +<?php + +/** + * A zipper is a purely-functional data structure which contains + * a focus that can be efficiently manipulated. It is known as + * a "one-hole context". This mutable variant implements a zipper + * for a list as a pair of two arrays, laid out as follows: + * + * Base list: 1 2 3 4 [ ] 6 7 8 9 + * Front list: 1 2 3 4 + * Back list: 9 8 7 6 + * + * User is expected to keep track of the "current element" and properly + * fill it back in as necessary. (ToDo: Maybe it's more user friendly + * to implicitly track the current element?) + * + * Nota bene: the current class gets confused if you try to store NULLs + * in the list. + */ + +class HTMLPurifier_Zipper +{ + public $front, $back; + + public function __construct($front, $back) { + $this->front = $front; + $this->back = $back; + } + + /** + * Creates a zipper from an array, with a hole in the + * 0-index position. + * @param Array to zipper-ify. + * @return Tuple of zipper and element of first position. + */ + static public function fromArray($array) { + $z = new self(array(), array_reverse($array)); + $t = $z->delete(); // delete the "dummy hole" + return array($z, $t); + } + + /** + * Convert zipper back into a normal array, optionally filling in + * the hole with a value. (Usually you should supply a $t, unless you + * are at the end of the array.) + */ + public function toArray($t = NULL) { + $a = $this->front; + if ($t !== NULL) $a[] = $t; + for ($i = count($this->back)-1; $i >= 0; $i--) { + $a[] = $this->back[$i]; + } + return $a; + } + + /** + * Move hole to the next element. + * @param $t Element to fill hole with + * @return Original contents of new hole. + */ + public function next($t) { + if ($t !== NULL) array_push($this->front, $t); + return empty($this->back) ? NULL : array_pop($this->back); + } + + /** + * Iterated hole advancement. + * @param $t Element to fill hole with + * @param $i How many forward to advance hole + * @return Original contents of new hole, i away + */ + public function advance($t, $n) { + for ($i = 0; $i < $n; $i++) { + $t = $this->next($t); + } + return $t; + } + + /** + * Move hole to the previous element + * @param $t Element to fill hole with + * @return Original contents of new hole. + */ + public function prev($t) { + if ($t !== NULL) array_push($this->back, $t); + return empty($this->front) ? NULL : array_pop($this->front); + } + + /** + * Delete contents of current hole, shifting hole to + * next element. + * @return Original contents of new hole. + */ + public function delete() { + return empty($this->back) ? NULL : array_pop($this->back); + } + + /** + * Returns true if we are at the end of the list. + * @return bool + */ + public function done() { + return empty($this->back); + } + + /** + * Insert element before hole. + * @param Element to insert + */ + public function insertBefore($t) { + if ($t !== NULL) array_push($this->front, $t); + } + + /** + * Insert element after hole. + * @param Element to insert + */ + public function insertAfter($t) { + if ($t !== NULL) array_push($this->back, $t); + } + + /** + * Splice in multiple elements at hole. Functional specification + * in terms of array_splice: + * + * $arr1 = $arr; + * $old1 = array_splice($arr1, $i, $delete, $replacement); + * + * list($z, $t) = HTMLPurifier_Zipper::fromArray($arr); + * $t = $z->advance($t, $i); + * list($old2, $t) = $z->splice($t, $delete, $replacement); + * $arr2 = $z->toArray($t); + * + * assert($old1 === $old2); + * assert($arr1 === $arr2); + * + * NB: the absolute index location after this operation is + * *unchanged!* + * + * @param Current contents of hole. + */ + public function splice($t, $delete, $replacement) { + // delete + $old = array(); + $r = $t; + for ($i = $delete; $i > 0; $i--) { + $old[] = $r; + $r = $this->delete(); + } + // insert + for ($i = count($replacement)-1; $i >= 0; $i--) { + $this->insertAfter($r); + $r = $replacement[$i]; + } + return array($old, $r); + } +} diff --git a/vendor/ezyang/htmlpurifier/maintenance/.htaccess b/vendor/ezyang/htmlpurifier/maintenance/.htaccess new file mode 100644 index 000000000..3a4288278 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/.htaccess @@ -0,0 +1 @@ +Deny from all diff --git a/vendor/ezyang/htmlpurifier/maintenance/PH5P.patch b/vendor/ezyang/htmlpurifier/maintenance/PH5P.patch new file mode 100644 index 000000000..763709509 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/PH5P.patch @@ -0,0 +1,102 @@ +--- C:\Users\Edward\Webs\htmlpurifier\maintenance\PH5P.php 2008-07-07 09:12:12.000000000 -0400 ++++ C:\Users\Edward\Webs\htmlpurifier\maintenance/PH5P.new.php 2008-12-06 02:29:34.988800000 -0500 +@@ -65,7 +65,7 @@ + + public function __construct($data) { + $data = str_replace("\r\n", "\n", $data); +- $date = str_replace("\r", null, $data); ++ $data = str_replace("\r", null, $data); + + $this->data = $data; + $this->char = -1; +@@ -211,7 +211,10 @@ + // If nothing is returned, emit a U+0026 AMPERSAND character token. + // Otherwise, emit the character token that was returned. + $char = (!$entity) ? '&' : $entity; +- $this->emitToken($char); ++ $this->emitToken(array( ++ 'type' => self::CHARACTR, ++ 'data' => $char ++ )); + + // Finally, switch to the data state. + $this->state = 'data'; +@@ -708,7 +711,7 @@ + } elseif($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ +- $this->entityInAttributeValueState('non'); ++ $this->entityInAttributeValueState(); + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) +@@ -738,7 +741,8 @@ + ? '&' + : $entity; + +- $this->emitToken($char); ++ $last = count($this->token['attr']) - 1; ++ $this->token['attr'][$last]['value'] .= $char; + } + + private function bogusCommentState() { +@@ -1066,6 +1070,11 @@ + $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; + } +@@ -2084,7 +2093,7 @@ + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + +- $this->insertElement($token); ++ $this->insertElement($token, true, true); + break; + } + break; +@@ -3465,7 +3474,18 @@ + } + } + +- private function insertElement($token, $append = true) { ++ 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) { +@@ -3659,7 +3679,7 @@ + } + } + +- private function generateImpliedEndTags(array $exclude = array()) { ++ 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 +@@ -3673,7 +3693,8 @@ + } + } + +- private function getElementCategory($name) { ++ private function getElementCategory($node) { ++ $name = $node->tagName; + if(in_array($name, $this->special)) + return self::SPECIAL; + diff --git a/vendor/ezyang/htmlpurifier/maintenance/PH5P.php b/vendor/ezyang/htmlpurifier/maintenance/PH5P.php new file mode 100644 index 000000000..9d83dcbf5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/PH5P.php @@ -0,0 +1,3889 @@ +<?php +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) + { + $data = str_replace("\r\n", "\n", $data); + $date = str_replace("\r", null, $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($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('non'); + + } 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; + + $this->emitToken($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 bellow. + 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)) { + $entity = $id; + break; + } + } + + $cond = isset($entity); + // The rest of the parsing happens bellow. + 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('&'.$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); + 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) + { + $el = $this->dom->createElement($token['name']); + + foreach($token['attr'] as $attr) { + if(!$el->hasAttribute($attr['name'])) { + $el->setAttribute($attr['name'], $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(array $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($name) + { + 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; + } +} diff --git a/vendor/ezyang/htmlpurifier/maintenance/add-vimline.php b/vendor/ezyang/htmlpurifier/maintenance/add-vimline.php new file mode 100644 index 000000000..d6a8eb202 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/add-vimline.php @@ -0,0 +1,130 @@ +#!/usr/bin/php +<?php + +chdir(dirname(__FILE__)); +require_once 'common.php'; +assertCli(); + +/** + * @file + * Adds vimline to files + */ + +chdir(dirname(__FILE__) . '/..'); +$FS = new FSTools(); + +$vimline = 'vim: et sw=4 sts=4'; + +$files = $FS->globr('.', '*'); +foreach ($files as $file) { + if ( + !is_file($file) || + prefix_is('./docs/doxygen', $file) || + prefix_is('./library/standalone', $file) || + prefix_is('./docs/specimens', $file) || + postfix_is('.ser', $file) || + postfix_is('.tgz', $file) || + postfix_is('.patch', $file) || + postfix_is('.dtd', $file) || + postfix_is('.ent', $file) || + postfix_is('.png', $file) || + postfix_is('.ico', $file) || + // wontfix + postfix_is('.vtest', $file) || + postfix_is('.svg', $file) || + postfix_is('.phpt', $file) || + postfix_is('VERSION', $file) || + postfix_is('WHATSNEW', $file) || + postfix_is('configdoc/usage.xml', $file) || + postfix_is('library/HTMLPurifier.includes.php', $file) || + postfix_is('library/HTMLPurifier.safe-includes.php', $file) || + postfix_is('smoketests/xssAttacks.xml', $file) || + // phpt files + postfix_is('.diff', $file) || + postfix_is('.exp', $file) || + postfix_is('.log', $file) || + postfix_is('.out', $file) || + + $file == './library/HTMLPurifier/Lexer/PH5P.php' || + $file == './maintenance/PH5P.php' + ) continue; + $ext = strrchr($file, '.'); + if ( + postfix_is('README', $file) || + postfix_is('LICENSE', $file) || + postfix_is('CREDITS', $file) || + postfix_is('INSTALL', $file) || + postfix_is('NEWS', $file) || + postfix_is('TODO', $file) || + postfix_is('WYSIWYG', $file) || + postfix_is('Changelog', $file) + ) $ext = '.txt'; + if (postfix_is('Doxyfile', $file)) $ext = 'Doxyfile'; + if (postfix_is('.php.in', $file)) $ext = '.php'; + $no_nl = false; + switch ($ext) { + case '.php': + case '.inc': + case '.js': + $line = '// %s'; + break; + case '.html': + case '.xsl': + case '.xml': + case '.htc': + $line = "<!-- %s\n-->"; + break; + case '.htmlt': + $no_nl = true; + $line = '--# %s'; + break; + case '.ini': + $line = '; %s'; + break; + case '.css': + $line = '/* %s */'; + break; + case '.bat': + $line = 'rem %s'; + break; + case '.txt': + case '.utf8': + if ( + prefix_is('./library/HTMLPurifier/ConfigSchema', $file) || + prefix_is('./smoketests/test-schema', $file) || + prefix_is('./tests/HTMLPurifier/StringHashParser', $file) + ) { + $no_nl = true; + $line = '--# %s'; + } else { + $line = ' %s'; + } + break; + case 'Doxyfile': + $line = '# %s'; + break; + default: + throw new Exception('Unknown file: ' . $file); + } + + echo "$file\n"; + $contents = file_get_contents($file); + + $regex = '~' . str_replace('%s', 'vim: .+', preg_quote($line, '~')) . '~m'; + $contents = preg_replace($regex, '', $contents); + + $contents = rtrim($contents); + + if (strpos($contents, "\r\n") !== false) $nl = "\r\n"; + elseif (strpos($contents, "\n") !== false) $nl = "\n"; + elseif (strpos($contents, "\r") !== false) $nl = "\r"; + else $nl = PHP_EOL; + + if (!$no_nl) $contents .= $nl; + $contents .= $nl . str_replace('%s', $vimline, $line) . $nl; + + file_put_contents($file, $contents); + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/maintenance/common.php b/vendor/ezyang/htmlpurifier/maintenance/common.php new file mode 100644 index 000000000..342bc205a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/common.php @@ -0,0 +1,25 @@ +<?php + +function assertCli() +{ + if (php_sapi_name() != 'cli' && !getenv('PHP_IS_CLI')) { + echo 'Script cannot be called from web-browser (if you are indeed calling via cli, +set environment variable PHP_IS_CLI to work around this).'; + exit(1); + } +} + +function prefix_is($comp, $subject) +{ + return strncmp($comp, $subject, strlen($comp)) === 0; +} + +function postfix_is($comp, $subject) +{ + return strlen($subject) < $comp ? false : substr($subject, -strlen($comp)) === $comp; +} + +// Load useful stuff like FSTools +require_once dirname(__FILE__) . '/../extras/HTMLPurifierExtras.auto.php'; + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/maintenance/compile-doxygen.sh b/vendor/ezyang/htmlpurifier/maintenance/compile-doxygen.sh new file mode 100755 index 000000000..ecd1127fd --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/compile-doxygen.sh @@ -0,0 +1,11 @@ +#!/bin/bash +cd .. +mkdir docs/doxygen +rm -Rf docs/doxygen/* +doxygen 1>docs/doxygen/info.log 2>docs/doxygen/errors.log +if [ "$?" != 0 ]; then + cat docs/doxygen/errors.log + exit +fi +cd docs +tar czf doxygen.tgz doxygen diff --git a/vendor/ezyang/htmlpurifier/maintenance/config-scanner.php b/vendor/ezyang/htmlpurifier/maintenance/config-scanner.php new file mode 100644 index 000000000..c614d1fbc --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/config-scanner.php @@ -0,0 +1,155 @@ +#!/usr/bin/php +<?php + +chdir(dirname(__FILE__)); +require_once 'common.php'; +require_once '../library/HTMLPurifier.auto.php'; +assertCli(); + +if (version_compare(PHP_VERSION, '5.2.2', '<')) { + echo "This script requires PHP 5.2.2 or later, for tokenizer line numbers."; + exit(1); +} + +/** + * @file + * Scans HTML Purifier source code for $config tokens and records the + * directive being used; configdoc can use this info later. + * + * Currently, this just dumps all the info onto the console. Eventually, it + * will create an XML file that our XSLT transform can use. + */ + +$FS = new FSTools(); +chdir(dirname(__FILE__) . '/../library/'); +$raw_files = $FS->globr('.', '*.php'); +$files = array(); +foreach ($raw_files as $file) { + $file = substr($file, 2); // rm leading './' + if (strncmp('standalone/', $file, 11) === 0) continue; // rm generated files + if (substr_count($file, '.') > 1) continue; // rm meta files + $files[] = $file; +} + +/** + * Moves the $i cursor to the next non-whitespace token + */ +function consumeWhitespace($tokens, &$i) +{ + do {$i++;} while (is_array($tokens[$i]) && $tokens[$i][0] === T_WHITESPACE); +} + +/** + * Tests whether or not a token is a particular type. There are three run-cases: + * - ($token, $expect_token): tests if the token is $expect_token type; + * - ($token, $expect_value): tests if the token is the string $expect_value; + * - ($token, $expect_token, $expect_value): tests if token is $expect_token type, and + * its string representation is $expect_value + */ +function testToken($token, $value_or_token, $value = null) +{ + if (is_null($value)) { + if (is_int($value_or_token)) return is_array($token) && $token[0] === $value_or_token; + else return $token === $value_or_token; + } else { + return is_array($token) && $token[0] === $value_or_token && $token[1] === $value; + } +} + +$counter = 0; +$full_counter = 0; +$tracker = array(); + +foreach ($files as $file) { + $tokens = token_get_all(file_get_contents($file)); + $file = str_replace('\\', '/', $file); + for ($i = 0, $c = count($tokens); $i < $c; $i++) { + $ok = false; + // Match $config + if (!$ok && testToken($tokens[$i], T_VARIABLE, '$config')) $ok = true; + // Match $this->config + while (!$ok && testToken($tokens[$i], T_VARIABLE, '$this')) { + consumeWhitespace($tokens, $i); + if (!testToken($tokens[$i], T_OBJECT_OPERATOR)) break; + consumeWhitespace($tokens, $i); + if (testToken($tokens[$i], T_STRING, 'config')) $ok = true; + break; + } + if (!$ok) continue; + + $ok = false; + for($i++; $i < $c; $i++) { + if ($tokens[$i] === ',' || $tokens[$i] === ')' || $tokens[$i] === ';') { + break; + } + if (is_string($tokens[$i])) continue; + if ($tokens[$i][0] === T_OBJECT_OPERATOR) { + $ok = true; + break; + } + } + if (!$ok) continue; + + $line = $tokens[$i][2]; + + consumeWhitespace($tokens, $i); + if (!testToken($tokens[$i], T_STRING, 'get')) continue; + + consumeWhitespace($tokens, $i); + if (!testToken($tokens[$i], '(')) continue; + + $full_counter++; + + $matched = false; + do { + + // What we currently don't match are batch retrievals, and + // wildcard retrievals. This data might be useful in the future, + // which is why we have a do {} while loop that doesn't actually + // do anything. + + consumeWhitespace($tokens, $i); + if (!testToken($tokens[$i], T_CONSTANT_ENCAPSED_STRING)) continue; + $id = substr($tokens[$i][1], 1, -1); + + $counter++; + $matched = true; + + if (!isset($tracker[$id])) $tracker[$id] = array(); + if (!isset($tracker[$id][$file])) $tracker[$id][$file] = array(); + $tracker[$id][$file][] = $line; + + } while (0); + + //echo "$file:$line uses $namespace.$directive\n"; + } +} + +echo "\n$counter/$full_counter instances of \$config or \$this->config found in source code.\n"; + +echo "Generating XML... "; + +$xw = new XMLWriter(); +$xw->openURI('../configdoc/usage.xml'); +$xw->setIndent(true); +$xw->startDocument('1.0', 'UTF-8'); +$xw->startElement('usage'); +foreach ($tracker as $id => $files) { + $xw->startElement('directive'); + $xw->writeAttribute('id', $id); + foreach ($files as $file => $lines) { + $xw->startElement('file'); + $xw->writeAttribute('name', $file); + foreach ($lines as $line) { + $xw->writeElement('line', $line); + } + $xw->endElement(); + } + $xw->endElement(); +} +$xw->endElement(); +$xw->flush(); + +echo "done!\n"; + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/maintenance/flush-definition-cache.php b/vendor/ezyang/htmlpurifier/maintenance/flush-definition-cache.php new file mode 100755 index 000000000..138badb65 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/flush-definition-cache.php @@ -0,0 +1,42 @@ +#!/usr/bin/php +<?php + +chdir(dirname(__FILE__)); +require_once 'common.php'; +assertCli(); + +/** + * @file + * Flushes the definition serial cache. This file should be + * called if changes to any subclasses of HTMLPurifier_Definition + * or related classes (such as HTMLPurifier_HTMLModule) are made. This + * may also be necessary if you've modified a customized version. + * + * @param Accepts one argument, cache type to flush; otherwise flushes all + * the caches. + */ + +echo "Flushing cache... \n"; + +require_once(dirname(__FILE__) . '/../library/HTMLPurifier.auto.php'); + +$config = HTMLPurifier_Config::createDefault(); + +$names = array('HTML', 'CSS', 'URI', 'Test'); +if (isset($argv[1])) { + if (in_array($argv[1], $names)) { + $names = array($argv[1]); + } else { + throw new Exception("Cache parameter {$argv[1]} is not a valid cache"); + } +} + +foreach ($names as $name) { + echo " - Flushing $name\n"; + $cache = new HTMLPurifier_DefinitionCache_Serializer($name); + $cache->flush($config); +} + +echo "Cache flushed successfully.\n"; + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/maintenance/flush.php b/vendor/ezyang/htmlpurifier/maintenance/flush.php new file mode 100644 index 000000000..c0853d230 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/flush.php @@ -0,0 +1,30 @@ +#!/usr/bin/php +<?php + +chdir(dirname(__FILE__)); +require_once 'common.php'; +assertCli(); + +/** + * @file + * Runs all generation/flush cache scripts to ensure that somewhat volatile + * generated files are up-to-date. + */ + +function e($cmd) +{ + echo "\$ $cmd\n"; + passthru($cmd, $status); + echo "\n"; + if ($status) exit($status); +} + +$php = empty($_SERVER['argv'][1]) ? 'php' : $_SERVER['argv'][1]; + +e($php . ' generate-includes.php'); +e($php . ' generate-schema-cache.php'); +e($php . ' flush-definition-cache.php'); +e($php . ' generate-standalone.php'); +e($php . ' config-scanner.php'); + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/maintenance/generate-entity-file.php b/vendor/ezyang/htmlpurifier/maintenance/generate-entity-file.php new file mode 100755 index 000000000..ff1713e39 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/generate-entity-file.php @@ -0,0 +1,75 @@ +#!/usr/bin/php +<?php + +chdir(dirname(__FILE__)); +require_once 'common.php'; +assertCli(); + +/** + * @file + * Parses *.ent files into an entity lookup table, and then serializes and + * writes the whole kaboodle to a file. The resulting file is cached so + * that this script does not need to be run. This script should rarely, + * if ever, be run, since HTML's entities are fairly immutable. + */ + +// here's where the entity files are located, assuming working directory +// is the same as the location of this PHP file. Needs trailing slash. +$entity_dir = '../docs/entities/'; + +// defines the output file for the serialized content. +$output_file = '../library/HTMLPurifier/EntityLookup/entities.ser'; + +// courtesy of a PHP manual comment +function unichr($dec) +{ + if ($dec < 128) { + $utf = chr($dec); + } elseif ($dec < 2048) { + $utf = chr(192 + (($dec - ($dec % 64)) / 64)); + $utf .= chr(128 + ($dec % 64)); + } else { + $utf = chr(224 + (($dec - ($dec % 4096)) / 4096)); + $utf .= chr(128 + ((($dec % 4096) - ($dec % 64)) / 64)); + $utf .= chr(128 + ($dec % 64)); + } + return $utf; +} + +if ( !is_dir($entity_dir) ) exit("Fatal Error: Can't find entity directory.\n"); +if ( file_exists($output_file) ) exit("Fatal Error: output file already exists.\n"); + +$dh = @opendir($entity_dir); +if ( !$dh ) exit("Fatal Error: Cannot read entity directory.\n"); + +$entity_files = array(); +while (($file = readdir($dh)) !== false) { + if (@$file[0] === '.') continue; + if (substr(strrchr($file, "."), 1) !== 'ent') continue; + $entity_files[] = $file; +} +closedir($dh); + +if ( !$entity_files ) exit("Fatal Error: No entity files to parse.\n"); + +$entity_table = array(); +$regexp = '/<!ENTITY\s+([A-Za-z0-9]+)\s+"&#(?:38;#)?([0-9]+);">/'; + +foreach ( $entity_files as $file ) { + $contents = file_get_contents($entity_dir . $file); + $matches = array(); + preg_match_all($regexp, $contents, $matches, PREG_SET_ORDER); + foreach ($matches as $match) { + $entity_table[$match[1]] = unichr($match[2]); + } +} + +$output = serialize($entity_table); + +$fh = fopen($output_file, 'w'); +fwrite($fh, $output); +fclose($fh); + +echo "Completed successfully."; + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/maintenance/generate-includes.php b/vendor/ezyang/htmlpurifier/maintenance/generate-includes.php new file mode 100644 index 000000000..01e1c2aba --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/generate-includes.php @@ -0,0 +1,192 @@ +#!/usr/bin/php +<?php + +chdir(dirname(__FILE__)); +require_once 'common.php'; +require_once '../tests/path2class.func.php'; +require_once '../library/HTMLPurifier/Bootstrap.php'; +assertCli(); + +/** + * @file + * Generates an include stub for users who do not want to use the autoloader. + * When new files are added to HTML Purifier's main codebase, this file should + * be called. + */ + +chdir(dirname(__FILE__) . '/../library/'); +$FS = new FSTools(); + +$exclude_dirs = array( + 'HTMLPurifier/Language/', + 'HTMLPurifier/ConfigSchema/', + 'HTMLPurifier/Filter/', + 'HTMLPurifier/Printer/', + /* These should be excluded, but need to have ConfigSchema support first + + */ +); +$exclude_files = array( + 'HTMLPurifier/Lexer/PEARSax3.php', + 'HTMLPurifier/Lexer/PH5P.php', + 'HTMLPurifier/Printer.php', +); + +// Determine what files need to be included: +echo 'Scanning for files... '; +$raw_files = $FS->globr('.', '*.php'); +if (!$raw_files) throw new Exception('Did not find any PHP source files'); +$files = array(); +foreach ($raw_files as $file) { + $file = substr($file, 2); // rm leading './' + if (strncmp('standalone/', $file, 11) === 0) continue; // rm generated files + if (substr_count($file, '.') > 1) continue; // rm meta files + $ok = true; + foreach ($exclude_dirs as $dir) { + if (strncmp($dir, $file, strlen($dir)) === 0) { + $ok = false; + break; + } + } + if (!$ok) continue; // rm excluded directories + if (in_array($file, $exclude_files)) continue; // rm excluded files + $files[] = $file; +} +echo "done!\n"; + +// Reorder list so that dependencies are included first: + +/** + * Returns a lookup array of dependencies for a file. + * + * @note This function expects that format $name extends $parent on one line + * + * @param string $file + * File to check dependencies of. + * @return array + * Lookup array of files the file is dependent on, sorted accordingly. + */ +function get_dependency_lookup($file) +{ + static $cache = array(); + if (isset($cache[$file])) return $cache[$file]; + if (!file_exists($file)) { + echo "File doesn't exist: $file\n"; + return array(); + } + $fh = fopen($file, 'r'); + $deps = array(); + while (!feof($fh)) { + $line = fgets($fh); + if (strncmp('class', $line, 5) === 0) { + // The implementation here is fragile and will break if we attempt + // to use interfaces. Beware! + $arr = explode(' extends ', trim($line, ' {'."\n\r"), 2); + if (count($arr) < 2) break; + $parent = $arr[1]; + $dep_file = HTMLPurifier_Bootstrap::getPath($parent); + if (!$dep_file) break; + $deps[$dep_file] = true; + break; + } + } + fclose($fh); + foreach (array_keys($deps) as $file) { + // Extra dependencies must come *before* base dependencies + $deps = get_dependency_lookup($file) + $deps; + } + $cache[$file] = $deps; + return $deps; +} + +/** + * Sorts files based on dependencies. This function is lazy and will not + * group files with dependencies together; it will merely ensure that a file + * is never included before its dependencies are. + * + * @param $files + * Files array to sort. + * @return + * Sorted array ($files is not modified by reference!) + */ +function dep_sort($files) +{ + $ret = array(); + $cache = array(); + foreach ($files as $file) { + if (isset($cache[$file])) continue; + $deps = get_dependency_lookup($file); + foreach (array_keys($deps) as $dep) { + if (!isset($cache[$dep])) { + $ret[] = $dep; + $cache[$dep] = true; + } + } + $cache[$file] = true; + $ret[] = $file; + } + return $ret; +} + +$files = dep_sort($files); + +// Build the actual include stub: + +$version = trim(file_get_contents('../VERSION')); + +// stub +$php = "<?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 $version + * + * @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. + */ + +"; + +foreach ($files as $file) { + $php .= "require '$file';" . PHP_EOL; +} + +echo "Writing HTMLPurifier.includes.php... "; +file_put_contents('HTMLPurifier.includes.php', $php); +echo "done!\n"; + +$php = "<?php + +/** + * @file + * This file was auto-generated by generate-includes.php and includes all of + * the core files required by HTML Purifier. This is a convenience stub that + * includes all files using dirname(__FILE__) and require_once. PLEASE DO NOT + * EDIT THIS FILE, changes will be overwritten the next time the script is run. + * + * Changes to include_path are not necessary. + */ + +\$__dir = dirname(__FILE__); + +"; + +foreach ($files as $file) { + $php .= "require_once \$__dir . '/$file';" . PHP_EOL; +} + +echo "Writing HTMLPurifier.safe-includes.php... "; +file_put_contents('HTMLPurifier.safe-includes.php', $php); +echo "done!\n"; + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/maintenance/generate-ph5p-patch.php b/vendor/ezyang/htmlpurifier/maintenance/generate-ph5p-patch.php new file mode 100644 index 000000000..c92a7d211 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/generate-ph5p-patch.php @@ -0,0 +1,22 @@ +<?php + +/** + * @file + * This file compares our version of PH5P with Jero's original version, and + * generates a patch of the differences. This script should be run whenever + * library/HTMLPurifier/Lexer/PH5P.php is modified. + */ + +$orig = realpath(dirname(__FILE__) . '/PH5P.php'); +$new = realpath(dirname(__FILE__) . '/../library/HTMLPurifier/Lexer/PH5P.php'); +$newt = dirname(__FILE__) . '/PH5P.new.php'; // temporary file + +// minor text-processing of new file to get into same format as original +$new_src = file_get_contents($new); +$new_src = '<?php' . PHP_EOL . substr($new_src, strpos($new_src, 'class HTML5 {')); + +file_put_contents($newt, $new_src); +shell_exec("diff -u \"$orig\" \"$newt\" > PH5P.patch"); +unlink($newt); + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/maintenance/generate-schema-cache.php b/vendor/ezyang/htmlpurifier/maintenance/generate-schema-cache.php new file mode 100644 index 000000000..339ff12da --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/generate-schema-cache.php @@ -0,0 +1,45 @@ +#!/usr/bin/php +<?php + +require_once dirname(__FILE__) . '/common.php'; +require_once dirname(__FILE__) . '/../library/HTMLPurifier.auto.php'; +assertCli(); + +/** + * @file + * Generates a schema cache file, saving it to + * library/HTMLPurifier/ConfigSchema/schema.ser. + * + * This should be run when new configuration options are added to + * HTML Purifier. A cached version is available via the repository + * so this does not normally have to be regenerated. + * + * If you have a directory containing custom configuration schema files, + * you can simple add a path to that directory as a parameter to + * this, and they will get included. + */ + +$target = dirname(__FILE__) . '/../library/HTMLPurifier/ConfigSchema/schema.ser'; + +$builder = new HTMLPurifier_ConfigSchema_InterchangeBuilder(); +$interchange = new HTMLPurifier_ConfigSchema_Interchange(); + +$builder->buildDir($interchange); + +$loader = dirname(__FILE__) . '/../config-schema.php'; +if (file_exists($loader)) include $loader; +foreach ($_SERVER['argv'] as $i => $dir) { + if ($i === 0) continue; + $builder->buildDir($interchange, realpath($dir)); +} + +$interchange->validate(); + +$schema_builder = new HTMLPurifier_ConfigSchema_Builder_ConfigSchema(); +$schema = $schema_builder->build($interchange); + +echo "Saving schema... "; +file_put_contents($target, serialize($schema)); +echo "done!\n"; + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/maintenance/generate-standalone.php b/vendor/ezyang/htmlpurifier/maintenance/generate-standalone.php new file mode 100755 index 000000000..254d4d83b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/generate-standalone.php @@ -0,0 +1,159 @@ +#!/usr/bin/php +<?php + +chdir(dirname(__FILE__)); +require_once 'common.php'; +assertCli(); + +/** + * @file + * Compiles all of HTML Purifier's library files into one big file + * named HTMLPurifier.standalone.php. This is usually called during the + * release process. + */ + +/** + * Global hash that tracks already loaded includes + */ +$GLOBALS['loaded'] = array(); + +/** + * Custom FSTools for this script that overloads some behavior + * @warning The overloading of copy() is not necessarily global for + * this script. Watch out! + */ +class MergeLibraryFSTools extends FSTools +{ + public function copyable($entry) + { + // Skip hidden files + if ($entry[0] == '.') { + return false; + } + return true; + } + public function copy($source, $dest) + { + copy_and_remove_includes($source, $dest); + } +} +$FS = new MergeLibraryFSTools(); + +/** + * Replaces the includes inside PHP source code with the corresponding + * source. + * @param string $text PHP source code to replace includes from + */ +function replace_includes($text) +{ + // also remove vim modelines + return preg_replace_callback( + "/require(?:_once)? ['\"]([^'\"]+)['\"];/", + 'replace_includes_callback', + $text + ); +} + +/** + * Removes leading PHP tags from included files. Assumes that there is + * no trailing tag. Also removes vim modelines. + * @note This is safe for files that have internal <?php + * @param string $text Text to have leading PHP tag from + */ +function remove_php_tags($text) +{ + $text = preg_replace('#// vim:.+#', '', $text); + return substr($text, 5); +} + +/** + * Copies the contents of a directory to the standalone directory + * @param string $dir Directory to copy + */ +function make_dir_standalone($dir) +{ + global $FS; + return $FS->copyr($dir, 'standalone/' . $dir); +} + +/** + * Copies the contents of a file to the standalone directory + * @param string $file File to copy + */ +function make_file_standalone($file) +{ + global $FS; + $FS->mkdirr('standalone/' . dirname($file)); + copy_and_remove_includes($file, 'standalone/' . $file); + return true; +} + +/** + * Copies a file to another location recursively, if it is a PHP file + * remove includes + * @param string $file Original file + * @param string $sfile New location of file + */ +function copy_and_remove_includes($file, $sfile) +{ + $contents = file_get_contents($file); + if (strrchr($file, '.') === '.php') $contents = replace_includes($contents); + return file_put_contents($sfile, $contents); +} + +/** + * @param $matches preg_replace_callback matches array, where index 1 + * is the filename to include + */ +function replace_includes_callback($matches) +{ + $file = $matches[1]; + $preserve = array( + // PEAR (external) + 'XML/HTMLSax3.php' => 1 + ); + if (isset($preserve[$file])) { + return $matches[0]; + } + if (isset($GLOBALS['loaded'][$file])) return ''; + $GLOBALS['loaded'][$file] = true; + return replace_includes(remove_php_tags(file_get_contents($file))); +} + +echo 'Generating includes file... '; +shell_exec('php generate-includes.php'); +echo "done!\n"; + +chdir(dirname(__FILE__) . '/../library/'); + +echo 'Creating full file...'; +$contents = replace_includes(file_get_contents('HTMLPurifier.includes.php')); +$contents = str_replace( + // Note that bootstrap is now inside the standalone file + "define('HTMLPURIFIER_PREFIX', realpath(dirname(__FILE__) . '/..'));", + "define('HTMLPURIFIER_PREFIX', dirname(__FILE__) . '/standalone'); + set_include_path(HTMLPURIFIER_PREFIX . PATH_SEPARATOR . get_include_path());", + $contents +); +file_put_contents('HTMLPurifier.standalone.php', $contents); +echo ' done!' . PHP_EOL; + +echo 'Creating standalone directory...'; +$FS->rmdirr('standalone'); // ensure a clean copy + +// data files +$FS->mkdirr('standalone/HTMLPurifier/DefinitionCache/Serializer'); +make_file_standalone('HTMLPurifier/EntityLookup/entities.ser'); +make_file_standalone('HTMLPurifier/ConfigSchema/schema.ser'); + +// non-standard inclusion setup +make_dir_standalone('HTMLPurifier/ConfigSchema'); +make_dir_standalone('HTMLPurifier/Language'); +make_dir_standalone('HTMLPurifier/Filter'); +make_dir_standalone('HTMLPurifier/Printer'); +make_file_standalone('HTMLPurifier/Printer.php'); +make_file_standalone('HTMLPurifier/Lexer/PH5P.php'); + +echo ' done!' . PHP_EOL; + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/maintenance/merge-library.php b/vendor/ezyang/htmlpurifier/maintenance/merge-library.php new file mode 100755 index 000000000..de2eecdc0 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/merge-library.php @@ -0,0 +1,11 @@ +#!/usr/bin/php +<?php + +/** + * @file + * Deprecated in favor of generate-standalone.php. + */ + +require dirname(__FILE__) . '/generate-standalone.php'; + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/maintenance/old-extract-schema.php b/vendor/ezyang/htmlpurifier/maintenance/old-extract-schema.php new file mode 100644 index 000000000..514a08dd9 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/old-extract-schema.php @@ -0,0 +1,71 @@ +#!/usr/bin/php +<?php + +chdir(dirname(__FILE__)); +require_once 'common.php'; +assertCli(); + +echo "Please do not run this script. It is here for historical purposes only."; +exit; + +/** + * @file + * Extracts all definitions inside a configuration schema + * (HTMLPurifier_ConfigSchema) and exports them as plain text files. + * + * @todo Extract version numbers. + */ + +define('HTMLPURIFIER_SCHEMA_STRICT', true); // description data needs to be collected +require_once dirname(__FILE__) . '/../library/HTMLPurifier.auto.php'; + +// We need includes to ensure all HTMLPurifier_ConfigSchema calls are +// performed. +require_once 'HTMLPurifier.includes.php'; + +// Also, these extra files will be necessary. +require_once 'HTMLPurifier/Filter/ExtractStyleBlocks.php'; + +/** + * Takes a hash and saves its contents to library/HTMLPurifier/ConfigSchema/ + */ +function saveHash($hash) +{ + if ($hash === false) return; + $dir = realpath(dirname(__FILE__) . '/../library/HTMLPurifier/ConfigSchema'); + $name = $hash['ID'] . '.txt'; + $file = $dir . '/' . $name; + if (file_exists($file)) { + trigger_error("File already exists; skipped $name"); + return; + } + $file = new FSTools_File($file); + $file->open('w'); + $multiline = false; + foreach ($hash as $key => $value) { + $multiline = $multiline || (strpos($value, "\n") !== false); + if ($multiline) { + $file->put("--$key--" . PHP_EOL); + $file->put(str_replace("\n", PHP_EOL, $value) . PHP_EOL); + } else { + if ($key == 'ID') { + $file->put("$value" . PHP_EOL); + } else { + $file->put("$key: $value" . PHP_EOL); + } + } + } + $file->close(); +} + +$schema = HTMLPurifier_ConfigSchema::instance(); +$adapter = new HTMLPurifier_ConfigSchema_StringHashReverseAdapter($schema); + +foreach ($schema->info as $ns => $ns_array) { + saveHash($adapter->get($ns)); + foreach ($ns_array as $dir => $x) { + saveHash($adapter->get($ns, $dir)); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/maintenance/old-remove-require-once.php b/vendor/ezyang/htmlpurifier/maintenance/old-remove-require-once.php new file mode 100644 index 000000000..f47c7d0f1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/old-remove-require-once.php @@ -0,0 +1,32 @@ +#!/usr/bin/php +<?php + +chdir(dirname(__FILE__)); +require_once 'common.php'; +assertCli(); + +echo "Please do not run this script. It is here for historical purposes only."; +exit; + +/** + * @file + * Removes leading includes from files. + * + * @note + * This does not remove inline includes; those must be handled manually. + */ + +chdir(dirname(__FILE__) . '/../tests/HTMLPurifier'); +$FS = new FSTools(); + +$files = $FS->globr('.', '*.php'); +foreach ($files as $file) { + if (substr_count(basename($file), '.') > 1) continue; + $old_code = file_get_contents($file); + $new_code = preg_replace("#^require_once .+[\n\r]*#m", '', $old_code); + if ($old_code !== $new_code) { + file_put_contents($file, $new_code); + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/maintenance/old-remove-schema-def.php b/vendor/ezyang/htmlpurifier/maintenance/old-remove-schema-def.php new file mode 100644 index 000000000..5ae031973 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/old-remove-schema-def.php @@ -0,0 +1,32 @@ +#!/usr/bin/php +<?php + +chdir(dirname(__FILE__)); +require_once 'common.php'; +assertCli(); + +echo "Please do not run this script. It is here for historical purposes only."; +exit; + +/** + * @file + * Removes ConfigSchema function calls from source files. + */ + +chdir(dirname(__FILE__) . '/../library/'); +$FS = new FSTools(); + +$files = $FS->globr('.', '*.php'); +foreach ($files as $file) { + if (substr_count(basename($file), '.') > 1) continue; + $old_code = file_get_contents($file); + $new_code = preg_replace("#^HTMLPurifier_ConfigSchema::.+?\);[\n\r]*#ms", '', $old_code); + if ($old_code !== $new_code) { + file_put_contents($file, $new_code); + } + if (preg_match('#^\s+HTMLPurifier_ConfigSchema::#m', $new_code)) { + echo "Indented ConfigSchema call in $file\n"; + } +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/maintenance/regenerate-docs.sh b/vendor/ezyang/htmlpurifier/maintenance/regenerate-docs.sh new file mode 100755 index 000000000..6f4d720ff --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/regenerate-docs.sh @@ -0,0 +1,5 @@ +#!/bin/bash -e +./compile-doxygen.sh +cd ../docs +scp doxygen.tgz htmlpurifier.org:/home/ezyang/htmlpurifier.org +ssh htmlpurifier.org "cd /home/ezyang/htmlpurifier.org && ./reload-docs.sh" diff --git a/vendor/ezyang/htmlpurifier/maintenance/remove-trailing-whitespace.php b/vendor/ezyang/htmlpurifier/maintenance/remove-trailing-whitespace.php new file mode 100644 index 000000000..857870546 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/remove-trailing-whitespace.php @@ -0,0 +1,37 @@ +#!/usr/bin/php +<?php + +chdir(dirname(__FILE__)); +require_once 'common.php'; +assertCli(); + +/** + * @file + * Removes trailing whitespace from files. + */ + +chdir(dirname(__FILE__) . '/..'); +$FS = new FSTools(); + +$files = $FS->globr('.', '{,.}*', GLOB_BRACE); +foreach ($files as $file) { + if ( + !is_file($file) || + prefix_is('./.git', $file) || + prefix_is('./docs/doxygen', $file) || + postfix_is('.ser', $file) || + postfix_is('.tgz', $file) || + postfix_is('.patch', $file) || + postfix_is('.dtd', $file) || + postfix_is('.ent', $file) || + $file == './library/HTMLPurifier/Lexer/PH5P.php' || + $file == './maintenance/PH5P.php' + ) continue; + $contents = file_get_contents($file); + $result = preg_replace('/^(.*?)[ \t]+(\r?)$/m', '\1\2', $contents, -1, $count); + if (!$count) continue; + echo "$file\n"; + file_put_contents($file, $result); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/maintenance/rename-config.php b/vendor/ezyang/htmlpurifier/maintenance/rename-config.php new file mode 100644 index 000000000..6e59e2a79 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/rename-config.php @@ -0,0 +1,84 @@ +#!/usr/bin/php +<?php + +chdir(dirname(__FILE__)); +require_once 'common.php'; +require_once '../library/HTMLPurifier.auto.php'; +assertCli(); + +/** + * @file + * Renames a configuration directive. This involves renaming the file, + * adding an alias, and then regenerating the cache. You still have to + * manually go through and fix any calls to the directive. + * @warning This script doesn't handle multi-stringhash files. + */ + +$argv = $_SERVER['argv']; +if (count($argv) < 3) { + echo "Usage: {$argv[0]} OldName NewName\n"; + exit(1); +} + +chdir('../library/HTMLPurifier/ConfigSchema/schema'); + +$old = $argv[1]; +$new = $argv[2]; + +if (!file_exists("$old.txt")) { + echo "Cannot move undefined configuration directive $old\n"; + exit(1); +} + +if ($old === $new) { + echo "Attempting to move to self, aborting\n"; + exit(1); +} + +if (file_exists("$new.txt")) { + echo "Cannot move to already defined directive $new\n"; + exit(1); +} + +$file = "$old.txt"; +$builder = new HTMLPurifier_ConfigSchema_InterchangeBuilder(); +$interchange = new HTMLPurifier_ConfigSchema_Interchange(); +$builder->buildFile($interchange, $file); +$contents = file_get_contents($file); + +if (strpos($contents, "\r\n") !== false) { + $nl = "\r\n"; +} elseif (strpos($contents, "\r") !== false) { + $nl = "\r"; +} else { + $nl = "\n"; +} + +// replace name with new name +$contents = str_replace($old, $new, $contents); + +if ($interchange->directives[$old]->aliases) { + $pos_alias = strpos($contents, 'ALIASES:'); + $pos_ins = strpos($contents, $nl, $pos_alias); + if ($pos_ins === false) $pos_ins = strlen($contents); + $contents = + substr($contents, 0, $pos_ins) . ", $old" . substr($contents, $pos_ins); + file_put_contents($file, $contents); +} else { + $lines = explode($nl, $contents); + $insert = false; + foreach ($lines as $n => $line) { + if (strncmp($line, '--', 2) === 0) { + $insert = $n; + break; + } + } + if (!$insert) { + $lines[] = "ALIASES: $old"; + } else { + array_splice($lines, $insert, 0, "ALIASES: $old"); + } + file_put_contents($file, implode($nl, $lines)); +} + +rename("$old.txt", "$new.txt") || exit(1); diff --git a/vendor/ezyang/htmlpurifier/maintenance/update-config.php b/vendor/ezyang/htmlpurifier/maintenance/update-config.php new file mode 100644 index 000000000..2d8a7a9c1 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/maintenance/update-config.php @@ -0,0 +1,34 @@ +#!/usr/bin/php +<?php + +chdir(dirname(__FILE__)); +require_once 'common.php'; +assertCli(); + +/** + * @file + * Converts all instances of $config->set and $config->get to the new + * format, as described by docs/dev-config-bcbreaks.txt + */ + +$FS = new FSTools(); +chdir(dirname(__FILE__) . '/..'); +$raw_files = $FS->globr('.', '*.php'); +foreach ($raw_files as $file) { + $file = substr($file, 2); // rm leading './' + if (strpos($file, 'library/standalone/') === 0) continue; + if (strpos($file, 'maintenance/update-config.php') === 0) continue; + if (strpos($file, 'test-settings.php') === 0) continue; + if (substr_count($file, '.') > 1) continue; // rm meta files + // process the file + $contents = file_get_contents($file); + $contents = preg_replace( + "#config->(set|get)\('(.+?)', '(.+?)'#", + "config->\\1('\\2.\\3'", + $contents + ); + if ($contents === '') continue; + file_put_contents($file, $contents); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/package.php b/vendor/ezyang/htmlpurifier/package.php new file mode 100644 index 000000000..bfef93622 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/package.php @@ -0,0 +1,61 @@ +<?php + +set_time_limit(0); + +require_once 'PEAR/PackageFileManager2.php'; +require_once 'PEAR/PackageFileManager/File.php'; +PEAR::setErrorHandling(PEAR_ERROR_PRINT); +$pkg = new PEAR_PackageFileManager2; + +$pkg->setOptions( + array( + 'baseinstalldir' => '/', + 'packagefile' => 'package.xml', + 'packagedirectory' => realpath(dirname(__FILE__) . '/library'), + 'filelistgenerator' => 'file', + 'include' => array('*'), + 'dir_roles' => array('/' => 'php'), // hack to put *.ser files in the right place + 'ignore' => array( + 'HTMLPurifier.standalone.php', + 'HTMLPurifier.path.php', + '*.tar.gz', + '*.tgz', + 'standalone/' + ), + ) +); + +$pkg->setPackage('HTMLPurifier'); +$pkg->setLicense('LGPL', 'http://www.gnu.org/licenses/lgpl.html'); +$pkg->setSummary('Standards-compliant HTML filter'); +$pkg->setDescription( + 'HTML Purifier is an HTML filter that will remove all malicious code + (better known as XSS) with a thoroughly audited, secure yet permissive + whitelist and will also make sure your documents are standards + compliant.' +); + +$pkg->addMaintainer('lead', 'ezyang', 'Edward Z. Yang', 'admin@htmlpurifier.org', 'yes'); + +$version = trim(file_get_contents('VERSION')); +$api_version = substr($version, 0, strrpos($version, '.')); + +$pkg->setChannel('htmlpurifier.org'); +$pkg->setAPIVersion($api_version); +$pkg->setAPIStability('stable'); +$pkg->setReleaseVersion($version); +$pkg->setReleaseStability('stable'); + +$pkg->addRelease(); + +$pkg->setNotes(file_get_contents('WHATSNEW')); +$pkg->setPackageType('php'); + +$pkg->setPhpDep('5.0.0'); +$pkg->setPearinstallerDep('1.4.3'); + +$pkg->generateContents(); + +$pkg->writePackageFile(); + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/phpdoc.ini b/vendor/ezyang/htmlpurifier/phpdoc.ini new file mode 100644 index 000000000..c4c372353 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/phpdoc.ini @@ -0,0 +1,102 @@ +;; phpDocumentor parse configuration file +;; +;; This file is designed to cut down on repetitive typing on the command-line or web interface +;; You can copy this file to create a number of configuration files that can be used with the +;; command-line switch -c, as in phpdoc -c default.ini or phpdoc -c myini.ini. The web +;; interface will automatically generate a list of .ini files that can be used. +;; +;; default.ini is used to generate the online manual at http://www.phpdoc.org/docs +;; +;; ALL .ini files must be in the user subdirectory of phpDocumentor with an extension of .ini +;; +;; Copyright 2002, Greg Beaver <cellog@users.sourceforge.net> +;; +;; WARNING: do not change the name of any command-line parameters, phpDocumentor will ignore them + +[Parse Data] +;; title of all the documentation +;; legal values: any string +title = HTML Purifier API Documentation + +;; parse files that start with a . like .bash_profile +;; legal values: true, false +hidden = false + +;; show elements marked @access private in documentation by setting this to on +;; legal values: on, off +parseprivate = off + +;; parse with javadoc-like description (first sentence is always the short description) +;; legal values: on, off +javadocdesc = on + +;; add any custom @tags separated by commas here +;; legal values: any legal tagname separated by commas. +;customtags = mytag1,mytag2 + +;; This is only used by the XML:DocBook/peardoc2 converter +defaultcategoryname = Documentation + +;; what is the main package? +;; legal values: alphanumeric string plus - and _ +defaultpackagename = HTMLPurifier + +;; output any parsing information? set to on for cron jobs +;; legal values: on +;quiet = on + +;; parse a PEAR-style repository. Do not turn this on if your project does +;; not have a parent directory named "pear" +;; legal values: on/off +;pear = on + +;; where should the documentation be written? +;; legal values: a legal path +target = docs/phpdoc + +;; Which files should be parsed out as special documentation files, such as README, +;; INSTALL and CHANGELOG? This overrides the default files found in +;; phpDocumentor.ini (this file is not a user .ini file, but the global file) +readmeinstallchangelog = README, INSTALL, NEWS, WYSIWYG, SLOW, LICENSE, CREDITS + +;; limit output to the specified packages, even if others are parsed +;; legal values: package names separated by commas +;packageoutput = package1,package2 + +;; comma-separated list of files to parse +;; legal values: paths separated by commas +;filename = /path/to/file1,/path/to/file2,fileincurrentdirectory + +;; comma-separated list of directories to parse +;; legal values: directory paths separated by commas +;directory = /path1,/path2,.,..,subdirectory +;directory = /home/jeichorn/cvs/pear +directory = . + +;; template base directory (the equivalent directory of <installdir>/phpDocumentor) +;templatebase = /path/to/my/templates + +;; directory to find any example files in through @example and {@example} tags +;examplesdir = /path/to/my/templates + +;; comma-separated list of files, directories or wildcards ? and * (any wildcard) to ignore +;; legal values: any wildcard strings separated by commas +;ignore = /path/to/ignore*,*list.php,myfile.php,subdirectory/ +ignore = *tests*,*benchmarks*,*docs*,*test-settings.php,*configdoc*,*maintenance*,*smoketests*,*standalone*,*.svn*,*conf* + +sourcecode = on + +;; comma-separated list of Converters to use in outputformat:Convertername:templatedirectory format +;; legal values: HTML:frames:default,HTML:frames:l0l33t,HTML:frames:phpdoc.de,HTML:frames:phphtmllib, +;; HTML:frames:earthli, +;; HTML:frames:DOM/default,HTML:frames:DOM/l0l33t,HTML:frames:DOM/phpdoc.de, +;; HTML:frames:DOM/phphtmllib,HTML:frames:DOM/earthli +;; HTML:Smarty:default,HTML:Smarty:PHP,HTML:Smarty:HandS +;; PDF:default:default,CHM:default:default,XML:DocBook/peardoc2:default +output=HTML:frames:default + +;; turn this option on if you want highlighted source code for every file +;; legal values: on/off +sourcecode = on + +; vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/plugins/modx.txt b/vendor/ezyang/htmlpurifier/plugins/modx.txt new file mode 100644 index 000000000..0763821b5 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/plugins/modx.txt @@ -0,0 +1,112 @@ + +MODx Plugin + +MODx <http://www.modxcms.com/> is an open source PHP application framework. +I first came across them in my referrer logs when tillda asked if anyone +could implement an HTML Purifier plugin. This forum thread +<http://modxcms.com/forums/index.php/topic,6604.0.html> eventually resulted +in the fruition of this plugin that davidm says, "is on top of my favorite +list." HTML Purifier goes great with WYSIWYG editors! + + + +1. Credits + +PaulGregory wrote the overall structure of the code. I added the +slashes hack. + + + +2. Install + +First, you need to place HTML Purifier library somewhere. The code here +assumes that you've placed in MODx's assets/plugins/htmlpurifier (no version +number). + +Log into the manager, and navigate: + +Resources > Manage Resources > Plugins tab > New Plugin + +Type in a name (probably HTML Purifier), and copy paste this code into the +textarea: + +-------------------------------------------------------------------------------- +$e = &$modx->Event; +if ($e->name == 'OnBeforeDocFormSave') { + global $content; + + include_once '../assets/plugins/htmlpurifier/library/HTMLPurifier.auto.php'; + $purifier = new HTMLPurifier(); + + static $magic_quotes = null; + if ($magic_quotes === null) { + // this is an ugly hack because this hook hasn't + // had the backslashes removed yet when magic_quotes_gpc is on, + // but HTMLPurifier must not have the quotes slashed. + $magic_quotes = get_magic_quotes_gpc(); + } + + if ($magic_quotes) $content = stripslashes($content); + $content = $purifier->purify($content); + if ($magic_quotes) $content = addslashes($content); +} +-------------------------------------------------------------------------------- + +Then navigate to the System Events tab and check "OnBeforeDocFormSave". +Save the plugin. HTML Purifier now is integrated! + + + +3. Making sure it works + +You can test HTML Purifier by deliberately putting in crappy HTML and seeing +whether or not it gets fixed. A better way is to put in something like this: + +<p lang="fr">Il est bon</p> + +...and seeing whether or not the content comes out as: + +<p lang="fr" xml:lang="fr">Il est bon</p> + +(lang to xml:lang synchronization is one of the many features HTML Purifier +has). + + + +4. Caveat Emptor + +This code does not intercept save requests from the QuickEdit plugin, this may +be added in a later version. It also modifies things on save, so there's a +slight chance that HTML Purifier may make a boo-boo and accidently mess things +up (the original version is not saved). + +Finally, make sure that MODx is using UTF-8. If you are using, say, a French +localisation, you may be using Latin-1, if that's the case, configure +HTML Purifier properly like this: + +$config = HTMLPurifier_Config::createDefault(); +$config->set('Core', 'Encoding', 'ISO-8859-1'); // or whatever encoding +$purifier = new HTMLPurifier($config); + + + +5. Known Bugs + +'rn' characters sometimes mysteriously appear after purification. We are +currently investigating this issue. See: <http://htmlpurifier.org/phorum/read.php?3,1866> + + + +6. See Also + +A modified version of Jot 1.1.3 is available, which integrates with HTML +Purifier. You can check it out here: <http://modxcms.com/forums/index.php/topic,25621.msg161970.html> + + +X. Changelog + +2008-06-16 +- Updated code to work with 3.1.0 and later +- Add Known Bugs and See Also section + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/plugins/phorum/.gitignore b/vendor/ezyang/htmlpurifier/plugins/phorum/.gitignore new file mode 100644 index 000000000..8325e0902 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/plugins/phorum/.gitignore @@ -0,0 +1,2 @@ +migrate.php +htmlpurifier/* diff --git a/vendor/ezyang/htmlpurifier/plugins/phorum/Changelog b/vendor/ezyang/htmlpurifier/plugins/phorum/Changelog new file mode 100644 index 000000000..9f939e54a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/plugins/phorum/Changelog @@ -0,0 +1,27 @@ +Changelog HTMLPurifier : Phorum Mod +||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + += KEY ==================== + # Breaks back-compat + ! Feature + - Bugfix + + Sub-comment + . Internal change +========================== + +Version 4.0.0 for Phorum 5.2, released July 9, 2009 +# Works only with HTML Purifier 4.0.0 +! Better installation documentation +- Fixed double encoded quotes +- Fixed fatal error when migrate.php is blank + +Version 3.0.0 for Phorum 5.2, released January 12, 2008 +# WYSIWYG and suppress_message options are now configurable via web + interface. +- Module now compatible with Phorum 5.2, primary bugs were in migration + code as well as signature and edit message handling. This module is NOT + compatible with Phorum 5.1. +- Buggy WYSIWYG mode refined +. AutoFormatParam added to list of default configuration namespaces + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/plugins/phorum/INSTALL b/vendor/ezyang/htmlpurifier/plugins/phorum/INSTALL new file mode 100644 index 000000000..23c76fc5c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/plugins/phorum/INSTALL @@ -0,0 +1,84 @@ + +Install + How to install the Phorum HTML Purifier plugin + +0. PREREQUISITES +---------------- +This Phorum module only works on PHP5 and with HTML Purifier 4.0.0 +or later. + +1. UNZIP +-------- +Unzip phorum-htmlpurifier-x.y.z, producing an htmlpurifier folder. +You've already done this step if you're reading this! + +2. MOVE +------- +Move the htmlpurifier folder to the mods/ folder of your Phorum +installation, so the directory structure looks like: + +phorum/ + mods/ + htmlpurifier/ + INSTALL - this install file + info.txt, ... - the module files + htmlpurifier/ + +3. INSTALL HTML PURIFIER +------------------------ +Download and unzip HTML Purifier <htmlpurifier.org>. Place the contents of +the library/ folder in the htmlpurifier/htmlpurifier folder. Your directory +structure will look like: + +phorum/ + mods/ + htmlpurifier/ + htmlpurifier/ + HTMLPurifier.auto.php + ... - other files + HTMLPurifier/ + +Advanced users: + If you have HTML Purifier installed elsewhere on your server, + all you need is an HTMLPurifier.auto.php file in the library folder which + includes the HTMLPurifier.auto.php file in your install. + +4. MIGRATE +---------- +If you're setting up a new Phorum installation, all you need to do is create +a blank migrate.php file in the htmlpurifier module folder (NOT the library +folder. + +If you have an old Phorum installation and was using BBCode, +copy migrate.bbcode.php to migrate.php. If you were using a different input +format, follow the instructions in migrate.bbcode.php to create your own custom +migrate.php file. + +Your directory structure should now look like this: + +phorum/ + mods/ + htmlpurifier/ + migrate.php + +5. ENABLE +--------- +Navigate to your Phorum admin panel at http://example.com/phorum/admin.php, +click on Global Settings > Modules, scroll to "HTML Purifier Phorum Mod" and +turn it On. + +6. MIGRATE SIGNATURES +--------------------- +If you're setting up a new Phorum installation, skip this step. + +If you allowed your users to make signatures, navigate to the module settings +page of HTML Purifier (Global Settings > Modules > HTML Purifier Phorum Mod > +Configure), type in "yes" in the "Confirm" box, and press "Migrate." + +ONLY DO THIS ONCE! BE SURE TO BACK UP YOUR DATABASE! + +7. CONFIGURE +------------ +Configure using Edit settings. See that page for more information. + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/plugins/phorum/README b/vendor/ezyang/htmlpurifier/plugins/phorum/README new file mode 100644 index 000000000..0524ed39d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/plugins/phorum/README @@ -0,0 +1,45 @@ + +HTML Purifier Phorum Mod - Filter your HTML the Standards-Compliant Way! + +This Phorum mod enables HTML posting on Phorum. Under normal circumstances, +this would cause a huge security risk, but because we are running +HTML through HTML Purifier, output is guaranteed to be XSS free and +standards-compliant. + +This mod requires HTML input, and previous markup languages need to be +converted accordingly. Thus, it is vital that you create a 'migrate.php' +file that works with your installation. If you're using the built-in +BBCode formatting, simply move migrate.bbcode.php to that place; for +other markup languages, consult said file for instructions on how +to adapt it to your needs. + + -- NOTE ------------------------------------------------- + You can also run this module in parallel with another + formatting module; this module attempts to place itself + at the end of the filtering chain. However, if any + previous modules produce insecure HTML (for instance, + a JavaScript email obfuscator) they will get cleaned. + +This module will not work if 'migrate.php' is not created, and an improperly +made migration file may *CORRUPT* Phorum, so please take your time to +do this correctly. It should go without saying to *BACKUP YOUR DATABASE* +before attempting anything here. If no migration is necessary, you can +simply create a blank migrate.php file. HTML Purifier is smart and will +not re-migrate already processed messages. However, the original code +is irretrievably lost (we may change this in the future.) + +This module will not automatically migrate user signatures, because this +process may take a long time. After installing the HTML Purifier module and +then configuring 'migrate.php', navigate to Settings and click 'Migrate +Signatures' to migrate all user signatures to HTML. + +All of HTML Purifier's usual functions are configurable via the mod settings +page. If you require custom configuration, create config.php file in +the mod directory that edits a $config variable. Be sure, also, to +set $PHORUM['mod_htmlpurifier']['wysiwyg'] to TRUE if you are using a +WYSIWYG editor (you can do this through a common hook or the web +configuration form). + +Visit HTML Purifier at <http://htmlpurifier.org/>. + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/plugins/phorum/config.default.php b/vendor/ezyang/htmlpurifier/plugins/phorum/config.default.php new file mode 100644 index 000000000..e047c0b42 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/plugins/phorum/config.default.php @@ -0,0 +1,57 @@ +<?php + +if(!defined("PHORUM")) exit; + +// default HTML Purifier configuration settings +$config->set('HTML.Allowed', + // alphabetically sorted +'a[href|title] +abbr[title] +acronym[title] +b +blockquote[cite] +br +caption +cite +code +dd +del +dfn +div +dl +dt +em +i +img[src|alt|title|class] +ins +kbd +li +ol +p +pre +s +strike +strong +sub +sup +table +tbody +td +tfoot +th +thead +tr +tt +u +ul +var'); +$config->set('AutoFormat.AutoParagraph', true); +$config->set('AutoFormat.Linkify', true); +$config->set('HTML.Doctype', 'XHTML 1.0 Transitional'); +$config->set('Core.AggressivelyFixLt', true); +$config->set('Core.Encoding', $GLOBALS['PHORUM']['DATA']['CHARSET']); // we'll change this eventually +if (strtolower($GLOBALS['PHORUM']['DATA']['CHARSET']) !== 'utf-8') { + $config->set('Core.EscapeNonASCIICharacters', true); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/plugins/phorum/htmlpurifier.php b/vendor/ezyang/htmlpurifier/plugins/phorum/htmlpurifier.php new file mode 100644 index 000000000..f66d8c36c --- /dev/null +++ b/vendor/ezyang/htmlpurifier/plugins/phorum/htmlpurifier.php @@ -0,0 +1,316 @@ +<?php + +/** + * HTML Purifier Phorum Mod. Filter your HTML the Standards-Compliant Way! + * + * This Phorum mod enables users to post raw HTML into Phorum. But never + * fear: with the help of HTML Purifier, this HTML will be beat into + * de-XSSed and standards-compliant form, safe for general consumption. + * It is not recommended, but possible to run this mod in parallel + * with other formatters (in short, please DISABLE the BBcode mod). + * + * For help migrating from your previous markup language to pure HTML + * please check the migrate.bbcode.php file. + * + * If you'd like to use this with a WYSIWYG editor, make sure that + * editor sets $PHORUM['mod_htmlpurifier']['wysiwyg'] to true. Otherwise, + * administrators who need to edit other people's comments may be at + * risk for some nasty attacks. + * + * Tested with Phorum 5.2.11. + */ + +// Note: Cache data is base64 encoded because Phorum insists on flinging +// to the user and expecting it to come back unharmed, newlines and +// all, which ain't happening. It's slower, it takes up more space, but +// at least it won't get mutilated + +/** + * Purifies a data array + */ +function phorum_htmlpurifier_format($data) +{ + $PHORUM = $GLOBALS["PHORUM"]; + + $purifier =& HTMLPurifier::getInstance(); + $cache_serial = $PHORUM['mod_htmlpurifier']['body_cache_serial']; + + foreach($data as $message_id => $message){ + if(isset($message['body'])) { + + if ($message_id) { + // we're dealing with a real message, not a fake, so + // there a number of shortcuts that can be taken + + if (isset($message['meta']['htmlpurifier_light'])) { + // format hook was called outside of Phorum's normal + // functions, do the abridged purification + $data[$message_id]['body'] = $purifier->purify($message['body']); + continue; + } + + if (!empty($PHORUM['args']['purge'])) { + // purge the cache, must be below the following if + unset($message['meta']['body_cache']); + } + + if ( + isset($message['meta']['body_cache']) && + isset($message['meta']['body_cache_serial']) && + $message['meta']['body_cache_serial'] == $cache_serial + ) { + // cached version is present, bail out early + $data[$message_id]['body'] = base64_decode($message['meta']['body_cache']); + continue; + } + } + + // migration might edit this array, that's why it's defined + // so early + $updated_message = array(); + + // create the $body variable + if ( + $message_id && // message must be real to migrate + !isset($message['meta']['body_cache_serial']) + ) { + // perform migration + $fake_data = array(); + list($signature, $edit_message) = phorum_htmlpurifier_remove_sig_and_editmessage($message); + $fake_data[$message_id] = $message; + $fake_data = phorum_htmlpurifier_migrate($fake_data); + $body = $fake_data[$message_id]['body']; + $body = str_replace("<phorum break>\n", "\n", $body); + $updated_message['body'] = $body; // save it in + $body .= $signature . $edit_message; // add it back in + } else { + // reverse Phorum's pre-processing + $body = $message['body']; + // order is important + $body = str_replace("<phorum break>\n", "\n", $body); + $body = str_replace(array('<','>','&', '"'), array('<','>','&','"'), $body); + if (!$message_id && defined('PHORUM_CONTROL_CENTER')) { + // we're in control.php, so it was double-escaped + $body = str_replace(array('<','>','&', '"'), array('<','>','&','"'), $body); + } + } + + $body = $purifier->purify($body); + + // dynamically update the cache (MUST BE DONE HERE!) + // this is inefficient because it's one db call per + // cache miss, but once the cache is in place things are + // a lot zippier. + + if ($message_id) { // make sure it's not a fake id + $updated_message['meta'] = $message['meta']; + $updated_message['meta']['body_cache'] = base64_encode($body); + $updated_message['meta']['body_cache_serial'] = $cache_serial; + phorum_db_update_message($message_id, $updated_message); + } + + // must not get overloaded until after we cache it, otherwise + // we'll inadvertently change the original text + $data[$message_id]['body'] = $body; + + } + } + + return $data; +} + +// ----------------------------------------------------------------------- +// This is fragile code, copied from read.php:596 (Phorum 5.2.6). Please +// keep this code in-sync with Phorum + +/** + * Generates a signature based on a message array + */ +function phorum_htmlpurifier_generate_sig($row) +{ + $phorum_sig = ''; + if(isset($row["user"]["signature"]) + && isset($row['meta']['show_signature']) && $row['meta']['show_signature']==1){ + $phorum_sig=trim($row["user"]["signature"]); + if(!empty($phorum_sig)){ + $phorum_sig="\n\n$phorum_sig"; + } + } + return $phorum_sig; +} + +/** + * Generates an edit message based on a message array + */ +function phorum_htmlpurifier_generate_editmessage($row) +{ + $PHORUM = $GLOBALS['PHORUM']; + $editmessage = ''; + if(isset($row['meta']['edit_count']) && $row['meta']['edit_count'] > 0) { + $editmessage = str_replace ("%count%", $row['meta']['edit_count'], $PHORUM["DATA"]["LANG"]["EditedMessage"]); + $editmessage = str_replace ("%lastedit%", phorum_date($PHORUM["short_date_time"],$row['meta']['edit_date']), $editmessage); + $editmessage = str_replace ("%lastuser%", $row['meta']['edit_username'], $editmessage); + $editmessage = "\n\n\n\n$editmessage"; + } + return $editmessage; +} + +// End fragile code +// ----------------------------------------------------------------------- + +/** + * Removes the signature and edit message from a message + * @param $row Message passed by reference + */ +function phorum_htmlpurifier_remove_sig_and_editmessage(&$row) +{ + $signature = phorum_htmlpurifier_generate_sig($row); + $editmessage = phorum_htmlpurifier_generate_editmessage($row); + $replacements = array(); + // we need to remove add <phorum break> as that is the form these + // extra bits are in. + if ($signature) $replacements[str_replace("\n", "<phorum break>\n", $signature)] = ''; + if ($editmessage) $replacements[str_replace("\n", "<phorum break>\n", $editmessage)] = ''; + $row['body'] = strtr($row['body'], $replacements); + return array($signature, $editmessage); +} + +/** + * Indicate that data is fully HTML and not from migration, invalidate + * previous caches + * @note This function could generate the actual cache entries, but + * since there's data missing that must be deferred to the first read + */ +function phorum_htmlpurifier_posting($message) +{ + $PHORUM = $GLOBALS["PHORUM"]; + unset($message['meta']['body_cache']); // invalidate the cache + $message['meta']['body_cache_serial'] = $PHORUM['mod_htmlpurifier']['body_cache_serial']; + return $message; +} + +/** + * Overload quoting mechanism to prevent default, mail-style quote from happening + */ +function phorum_htmlpurifier_quote($array) +{ + $PHORUM = $GLOBALS["PHORUM"]; + $purifier =& HTMLPurifier::getInstance(); + $text = $purifier->purify($array[1]); + $source = htmlspecialchars($array[0]); + return "<blockquote cite=\"$source\">\n$text\n</blockquote>"; +} + +/** + * Ensure that our format hook is processed last. Also, loads the library. + * @credits <http://secretsauce.phorum.org/snippets/make_bbcode_last_formatter.php.txt> + */ +function phorum_htmlpurifier_common() +{ + require_once(dirname(__FILE__).'/htmlpurifier/HTMLPurifier.auto.php'); + require(dirname(__FILE__).'/init-config.php'); + + $config = phorum_htmlpurifier_get_config(); + HTMLPurifier::getInstance($config); + + // increment revision.txt if you want to invalidate the cache + $GLOBALS['PHORUM']['mod_htmlpurifier']['body_cache_serial'] = $config->getSerial(); + + // load migration + if (file_exists(dirname(__FILE__) . '/migrate.php')) { + include(dirname(__FILE__) . '/migrate.php'); + } else { + echo '<strong>Error:</strong> No migration path specified for HTML Purifier, please check + <tt>modes/htmlpurifier/migrate.bbcode.php</tt> for instructions on + how to migrate from your previous markup language.'; + exit; + } + + if (!function_exists('phorum_htmlpurifier_migrate')) { + // Dummy function + function phorum_htmlpurifier_migrate($data) {return $data;} + } + +} + +/** + * Pre-emptively performs purification if it looks like a WYSIWYG editor + * is being used + */ +function phorum_htmlpurifier_before_editor($message) +{ + if (!empty($GLOBALS['PHORUM']['mod_htmlpurifier']['wysiwyg'])) { + if (!empty($message['body'])) { + $body = $message['body']; + // de-entity-ize contents + $body = str_replace(array('<','>','&'), array('<','>','&'), $body); + $purifier =& HTMLPurifier::getInstance(); + $body = $purifier->purify($body); + // re-entity-ize contents + $body = htmlspecialchars($body, ENT_QUOTES, $GLOBALS['PHORUM']['DATA']['CHARSET']); + $message['body'] = $body; + } + } + return $message; +} + +function phorum_htmlpurifier_editor_after_subject() +{ + // don't show this message if it's a WYSIWYG editor, since it will + // then be handled automatically + if (!empty($GLOBALS['PHORUM']['mod_htmlpurifier']['wysiwyg'])) { + $i = $GLOBALS['PHORUM']['DATA']['MODE']; + if ($i == 'quote' || $i == 'edit' || $i == 'moderation') { + ?> + <div> + <p> + <strong>Notice:</strong> HTML has been scrubbed for your safety. + If you would like to see the original, turn off WYSIWYG mode + (consult your administrator for details.) + </p> + </div> + <?php + } + return; + } + if (!empty($GLOBALS['PHORUM']['mod_htmlpurifier']['suppress_message'])) return; + ?><div class="htmlpurifier-help"> + <p> + <strong>HTML input</strong> is enabled. Make sure you escape all HTML and + angled brackets with <code>&lt;</code> and <code>&gt;</code>. + </p><?php + $purifier =& HTMLPurifier::getInstance(); + $config = $purifier->config; + if ($config->get('AutoFormat.AutoParagraph')) { + ?><p> + <strong>Auto-paragraphing</strong> is enabled. Double + newlines will be converted to paragraphs; for single + newlines, use the <code>pre</code> tag. + </p><?php + } + $html_definition = $config->getDefinition('HTML'); + $allowed = array(); + foreach ($html_definition->info as $name => $x) $allowed[] = "<code>$name</code>"; + sort($allowed); + $allowed_text = implode(', ', $allowed); + ?><p><strong>Allowed tags:</strong> <?php + echo $allowed_text; + ?>.</p><?php + ?> + </p> + <p> + For inputting literal code such as HTML and PHP for display, use + CDATA tags to auto-escape your angled brackets, and <code>pre</code> + to preserve newlines: + </p> + <pre><pre><![CDATA[ +<em>Place code here</em> +]]></pre></pre> + <p> + Power users, you can hide this notice with: + <pre>.htmlpurifier-help {display:none;}</pre> + </p> + </div><?php +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/plugins/phorum/info.txt b/vendor/ezyang/htmlpurifier/plugins/phorum/info.txt new file mode 100644 index 000000000..723465490 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/plugins/phorum/info.txt @@ -0,0 +1,18 @@ +title: HTML Purifier Phorum Mod +desc: This module enables standards-compliant HTML filtering on Phorum. Please check migrate.bbcode.php before enabling this mod. +author: Edward Z. Yang +url: http://htmlpurifier.org/ +version: 4.0.0 + +hook: format|phorum_htmlpurifier_format +hook: quote|phorum_htmlpurifier_quote +hook: posting_custom_action|phorum_htmlpurifier_posting +hook: common|phorum_htmlpurifier_common +hook: before_editor|phorum_htmlpurifier_before_editor +hook: tpl_editor_after_subject|phorum_htmlpurifier_editor_after_subject + +# This module is meant to be a drop-in for bbcode, so make it run last. +priority: run module after * +priority: run hook format after * + + vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/plugins/phorum/init-config.php b/vendor/ezyang/htmlpurifier/plugins/phorum/init-config.php new file mode 100644 index 000000000..e19787b4b --- /dev/null +++ b/vendor/ezyang/htmlpurifier/plugins/phorum/init-config.php @@ -0,0 +1,30 @@ +<?php + +/** + * Initializes the appropriate configuration from either a PHP file + * or a module configuration value + * @return Instance of HTMLPurifier_Config + */ +function phorum_htmlpurifier_get_config($default = false) +{ + global $PHORUM; + $config_exists = phorum_htmlpurifier_config_file_exists(); + if ($default || $config_exists || !isset($PHORUM['mod_htmlpurifier']['config'])) { + $config = HTMLPurifier_Config::createDefault(); + include(dirname(__FILE__) . '/config.default.php'); + if ($config_exists) { + include(dirname(__FILE__) . '/config.php'); + } + unset($PHORUM['mod_htmlpurifier']['config']); // unnecessary + } else { + $config = HTMLPurifier_Config::create($PHORUM['mod_htmlpurifier']['config']); + } + return $config; +} + +function phorum_htmlpurifier_config_file_exists() +{ + return file_exists(dirname(__FILE__) . '/config.php'); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/plugins/phorum/migrate.bbcode.php b/vendor/ezyang/htmlpurifier/plugins/phorum/migrate.bbcode.php new file mode 100644 index 000000000..0d0919455 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/plugins/phorum/migrate.bbcode.php @@ -0,0 +1,31 @@ +<?php + +/** + * This file is responsible for migrating from a specific markup language + * like BBCode or Markdown to HTML. WARNING: THIS PROCESS IS NOT REVERSIBLE + * + * Copy this file to 'migrate.php' and it will automatically work for + * BBCode; you may need to tweak this a little to get it to work for other + * languages (usually, just replace the include name and the function name). + * + * If you do NOT want to have any migration performed (for instance, you + * are installing the module on a new forum with no posts), simply remove + * phorum_htmlpurifier_migrate() function. You still need migrate.php + * present, otherwise the module won't work. This ensures that the user + * explicitly says, "No, I do not need to migrate." + */ + +if(!defined("PHORUM")) exit; + +require_once(dirname(__FILE__) . "/../bbcode/bbcode.php"); + +/** + * 'format' hook style function that will be called to convert + * legacy markup into HTML. + */ +function phorum_htmlpurifier_migrate($data) +{ + return phorum_mod_bbcode_format($data); // bbcode's 'format' hook +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/plugins/phorum/settings.php b/vendor/ezyang/htmlpurifier/plugins/phorum/settings.php new file mode 100644 index 000000000..8158f0282 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/plugins/phorum/settings.php @@ -0,0 +1,64 @@ +<?php + +// based off of BBCode's settings file + +/** + * HTML Purifier Phorum mod settings configuration. This provides + * a convenient web-interface for editing the most common HTML Purifier + * configuration directives. You can also specify custom configuration + * by creating a 'config.php' file. + */ + +if(!defined("PHORUM_ADMIN")) exit; + +// error reporting is good! +error_reporting(E_ALL ^ E_NOTICE); + +// load library and other paraphenalia +require_once './include/admin/PhorumInputForm.php'; +require_once (dirname(__FILE__) . '/htmlpurifier/HTMLPurifier.auto.php'); +require_once (dirname(__FILE__) . '/init-config.php'); +require_once (dirname(__FILE__) . '/settings/migrate-sigs-form.php'); +require_once (dirname(__FILE__) . '/settings/migrate-sigs.php'); +require_once (dirname(__FILE__) . '/settings/form.php'); +require_once (dirname(__FILE__) . '/settings/save.php'); + +// define friendly configuration directives. you can expand this array +// to get more web-definable directives +$PHORUM['mod_htmlpurifier']['directives'] = array( + 'URI.Host', // auto-detectable + 'URI.DisableExternal', + 'URI.DisableExternalResources', + 'URI.DisableResources', + 'URI.Munge', + 'URI.HostBlacklist', + 'URI.Disable', + 'HTML.TidyLevel', + 'HTML.Doctype', // auto-detectable + 'HTML.Allowed', + 'AutoFormat', + '-AutoFormat.Custom', + 'AutoFormatParam', + 'Output.TidyFormat', +); + +// lower this setting if you're getting time outs/out of memory +$PHORUM['mod_htmlpurifier']['migrate-sigs-increment'] = 100; + +if (isset($_POST['reset'])) { + unset($PHORUM['mod_htmlpurifier']['config']); +} + +if ($offset = phorum_htmlpurifier_migrate_sigs_check()) { + // migrate signatures + phorum_htmlpurifier_migrate_sigs($offset); +} elseif(!empty($_POST)){ + // save settings + phorum_htmlpurifier_save_settings(); +} + +phorum_htmlpurifier_show_migrate_sigs_form(); +echo '<br />'; +phorum_htmlpurifier_show_form(); + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/plugins/phorum/settings/form.php b/vendor/ezyang/htmlpurifier/plugins/phorum/settings/form.php new file mode 100644 index 000000000..9b6ad5f39 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/plugins/phorum/settings/form.php @@ -0,0 +1,95 @@ +<?php + +function phorum_htmlpurifier_show_form() +{ + if (phorum_htmlpurifier_config_file_exists()) { + phorum_htmlpurifier_show_config_info(); + return; + } + + global $PHORUM; + + $config = phorum_htmlpurifier_get_config(); + + $frm = new PhorumInputForm ("", "post", "Save"); + $frm->hidden("module", "modsettings"); + $frm->hidden("mod", "htmlpurifier"); // this is the directory name that the Settings file lives in + + if (!empty($error)){ + echo "$error<br />"; + } + + $frm->addbreak("Edit settings for the HTML Purifier module"); + + $frm->addMessage('<p>The box below sets <code>$PHORUM[\'mod_htmlpurifier\'][\'wysiwyg\']</code>. + When checked, contents sent for edit are now purified and the + informative message is disabled. If your WYSIWYG editor is disabled for + admin edits, you can safely keep this unchecked.</p>'); + $frm->addRow('Use WYSIWYG?', $frm->checkbox('wysiwyg', '1', '', $PHORUM['mod_htmlpurifier']['wysiwyg'])); + + $frm->addMessage('<p>The box below sets <code>$PHORUM[\'mod_htmlpurifier\'][\'suppress_message\']</code>, + which removes the big how-to use + HTML Purifier message.</p>'); + $frm->addRow('Suppress information?', $frm->checkbox('suppress_message', '1', '', $PHORUM['mod_htmlpurifier']['suppress_message'])); + + $frm->addMessage('<p>Click on directive links to read what each option does + (links do not open in new windows).</p> + <p>For more flexibility (for instance, you want to edit the full + range of configuration directives), you can create a <tt>config.php</tt> + file in your <tt>mods/htmlpurifier/</tt> directory. Doing so will, + however, make the web configuration interface unavailable.</p>'); + + require_once 'HTMLPurifier/Printer/ConfigForm.php'; + $htmlpurifier_form = new HTMLPurifier_Printer_ConfigForm('config', 'http://htmlpurifier.org/live/configdoc/plain.html#%s'); + $htmlpurifier_form->setTextareaDimensions(23, 7); // widen a little, since we have space + + $frm->addMessage($htmlpurifier_form->render( + $config, $PHORUM['mod_htmlpurifier']['directives'], false)); + + $frm->addMessage("<strong>Warning: Changing HTML Purifier's configuration will invalidate + the cache. Expect to see a flurry of database activity after you change + any of these settings.</strong>"); + + $frm->addrow('Reset to defaults:', $frm->checkbox("reset", "1", "", false)); + + // hack to include extra styling + echo '<style type="text/css">' . $htmlpurifier_form->getCSS() . ' + .hp-config {margin-left:auto;margin-right:auto;} + </style>'; + $js = $htmlpurifier_form->getJavaScript(); + echo '<script type="text/javascript">'."<!--\n$js\n//-->".'</script>'; + + $frm->show(); +} + +function phorum_htmlpurifier_show_config_info() +{ + global $PHORUM; + + // update mod_htmlpurifier for housekeeping + phorum_htmlpurifier_commit_settings(); + + // politely tell user how to edit settings manually +?> + <div class="input-form-td-break">How to edit settings for HTML Purifier module</div> + <p> + A <tt>config.php</tt> file exists in your <tt>mods/htmlpurifier/</tt> + directory. This file contains your custom configuration: in order to + change it, please navigate to that file and edit it accordingly. + You can also set <code>$GLOBALS['PHORUM']['mod_htmlpurifier']['wysiwyg']</code> + or <code>$GLOBALS['PHORUM']['mod_htmlpurifier']['suppress_message']</code> + </p> + <p> + To use the web interface, delete <tt>config.php</tt> (or rename it to + <tt>config.php.bak</tt>). + </p> + <p> + <strong>Warning: Changing HTML Purifier's configuration will invalidate + the cache. Expect to see a flurry of database activity after you change + any of these settings.</strong> + </p> +<?php + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/plugins/phorum/settings/migrate-sigs-form.php b/vendor/ezyang/htmlpurifier/plugins/phorum/settings/migrate-sigs-form.php new file mode 100644 index 000000000..abea3b51d --- /dev/null +++ b/vendor/ezyang/htmlpurifier/plugins/phorum/settings/migrate-sigs-form.php @@ -0,0 +1,22 @@ +<?php + +function phorum_htmlpurifier_show_migrate_sigs_form() +{ + $frm = new PhorumInputForm ('', "post", "Migrate"); + $frm->hidden("module", "modsettings"); + $frm->hidden("mod", "htmlpurifier"); + $frm->hidden("migrate-sigs", "1"); + $frm->addbreak("Migrate user signatures to HTML"); + $frm->addMessage('This operation will migrate your users signatures + to HTML. <strong>This process is irreversible and must only be performed once.</strong> + Type in yes in the confirmation field to migrate.'); + if (!file_exists(dirname(__FILE__) . '/../migrate.php')) { + $frm->addMessage('Migration file does not exist, cannot migrate signatures. + Please check <tt>migrate.bbcode.php</tt> on how to create an appropriate file.'); + } else { + $frm->addrow('Confirm:', $frm->text_box("confirmation", "")); + } + $frm->show(); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/plugins/phorum/settings/migrate-sigs.php b/vendor/ezyang/htmlpurifier/plugins/phorum/settings/migrate-sigs.php new file mode 100644 index 000000000..5ea9cd0b8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/plugins/phorum/settings/migrate-sigs.php @@ -0,0 +1,79 @@ +<?php + +function phorum_htmlpurifier_migrate_sigs_check() +{ + global $PHORUM; + $offset = 0; + if (!empty($_POST['migrate-sigs'])) { + if (!isset($_POST['confirmation']) || strtolower($_POST['confirmation']) !== 'yes') { + echo 'Invalid confirmation code.'; + exit; + } + $PHORUM['mod_htmlpurifier']['migrate-sigs'] = true; + phorum_db_update_settings(array("mod_htmlpurifier"=>$PHORUM["mod_htmlpurifier"])); + $offset = 1; + } elseif (!empty($_GET['migrate-sigs']) && $PHORUM['mod_htmlpurifier']['migrate-sigs']) { + $offset = (int) $_GET['migrate-sigs']; + } + return $offset; +} + +function phorum_htmlpurifier_migrate_sigs($offset) +{ + global $PHORUM; + + if(!$offset) return; // bail out quick if $offset == 0 + + // theoretically, we could get rid of this multi-request + // doo-hickery if safe mode is off + @set_time_limit(0); // attempt to let this run + $increment = $PHORUM['mod_htmlpurifier']['migrate-sigs-increment']; + + require_once(dirname(__FILE__) . '/../migrate.php'); + // migrate signatures + // do this in batches so we don't run out of time/space + $end = $offset + $increment; + $user_ids = array(); + for ($i = $offset; $i < $end; $i++) { + $user_ids[] = $i; + } + $userinfos = phorum_db_user_get_fields($user_ids, 'signature'); + foreach ($userinfos as $i => $user) { + if (empty($user['signature'])) continue; + $sig = $user['signature']; + // perform standard Phorum processing on the sig + $sig = str_replace(array("&","<",">"), array("&","<",">"), $sig); + $sig = preg_replace("/<((http|https|ftp):\/\/[a-z0-9;\/\?:@=\&\$\-_\.\+!*'\(\),~%]+?)>/i", "$1", $sig); + // prepare fake data to pass to migration function + $fake_data = array(array("author"=>"", "email"=>"", "subject"=>"", 'body' => $sig)); + list($fake_message) = phorum_htmlpurifier_migrate($fake_data); + $user['signature'] = $fake_message['body']; + if (!phorum_api_user_save($user)) { + exit('Error while saving user data'); + } + } + unset($userinfos); // free up memory + + // query for highest ID in database + $type = $PHORUM['DBCONFIG']['type']; + $sql = "select MAX(user_id) from {$PHORUM['user_table']}"; + $row = phorum_db_interact(DB_RETURN_ROW, $sql); + $top_id = (int) $row[0]; + + $offset += $increment; + if ($offset > $top_id) { // test for end condition + echo 'Migration finished'; + $PHORUM['mod_htmlpurifier']['migrate-sigs'] = false; + phorum_htmlpurifier_commit_settings(); + return true; + } + $host = $_SERVER['HTTP_HOST']; + $uri = rtrim(dirname($_SERVER['PHP_SELF']), '/\\'); + $extra = 'admin.php?module=modsettings&mod=htmlpurifier&migrate-sigs=' . $offset; + // relies on output buffering to work + header("Location: http://$host$uri/$extra"); + exit; + +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/plugins/phorum/settings/save.php b/vendor/ezyang/htmlpurifier/plugins/phorum/settings/save.php new file mode 100644 index 000000000..2aefaf83a --- /dev/null +++ b/vendor/ezyang/htmlpurifier/plugins/phorum/settings/save.php @@ -0,0 +1,29 @@ +<?php + +function phorum_htmlpurifier_save_settings() +{ + global $PHORUM; + if (phorum_htmlpurifier_config_file_exists()) { + echo "Cannot update settings, <code>mods/htmlpurifier/config.php</code> already exists. To change + settings, edit that file. To use the web form, delete that file.<br />"; + } else { + $config = phorum_htmlpurifier_get_config(true); + if (!isset($_POST['reset'])) $config->mergeArrayFromForm($_POST, 'config', $PHORUM['mod_htmlpurifier']['directives']); + $PHORUM['mod_htmlpurifier']['config'] = $config->getAll(); + } + $PHORUM['mod_htmlpurifier']['wysiwyg'] = !empty($_POST['wysiwyg']); + $PHORUM['mod_htmlpurifier']['suppress_message'] = !empty($_POST['suppress_message']); + if(!phorum_htmlpurifier_commit_settings()){ + $error="Database error while updating settings."; + } else { + echo "Settings Updated<br />"; + } +} + +function phorum_htmlpurifier_commit_settings() +{ + global $PHORUM; + return phorum_db_update_settings(array("mod_htmlpurifier"=>$PHORUM["mod_htmlpurifier"])); +} + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/release1-update.php b/vendor/ezyang/htmlpurifier/release1-update.php new file mode 100644 index 000000000..834d38567 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/release1-update.php @@ -0,0 +1,110 @@ +<?php + +// release script +// PHP 5.0 only + +if (php_sapi_name() != 'cli') { + echo 'Release script cannot be called from web-browser.'; + exit; +} + +if (!isset($argv[1])) { + echo +'php release.php [version] + HTML Purifier release script +'; + exit; +} + +$version = trim($argv[1]); + +// Bump version numbers: + +// ...in VERSION +file_put_contents('VERSION', $version); + +// ...in NEWS +if ($is_dev = (strpos($version, 'dev') === false)) { + $date = date('Y-m-d'); + $news_c = str_replace( + $l = "$version, unknown release date", + "$version, released $date", + file_get_contents('NEWS'), + $c + ); + if (!$c) { + echo 'Could not update NEWS, missing ' . $l . PHP_EOL; + exit; + } elseif ($c > 1) { + echo 'More than one release declaration in NEWS replaced' . PHP_EOL; + exit; + } + file_put_contents('NEWS', $news_c); +} + +// ...in Doxyfile +$doxyfile_c = preg_replace( + '/(?<=PROJECT_NUMBER {9}= )[^\s]+/m', // brittle + $version, + file_get_contents('Doxyfile'), + 1, $c +); +if (!$c) { + echo 'Could not update Doxyfile, missing PROJECT_NUMBER.' . PHP_EOL; + exit; +} +file_put_contents('Doxyfile', $doxyfile_c); + +// ...in HTMLPurifier.php +$htmlpurifier_c = file_get_contents('library/HTMLPurifier.php'); +$htmlpurifier_c = preg_replace( + '/HTML Purifier .+? - /', + "HTML Purifier $version - ", + $htmlpurifier_c, + 1, $c +); +if (!$c) { + echo 'Could not update HTMLPurifier.php, missing HTML Purifier [version] header.' . PHP_EOL; + exit; +} +$htmlpurifier_c = preg_replace( + '/public \$version = \'.+?\';/', + "public \$version = '$version';", + $htmlpurifier_c, + 1, $c +); +if (!$c) { + echo 'Could not update HTMLPurifier.php, missing public $version.' . PHP_EOL; + exit; +} +$htmlpurifier_c = preg_replace( + '/const VERSION = \'.+?\';/', + "const VERSION = '$version';", + $htmlpurifier_c, + 1, $c +); +if (!$c) { + echo 'Could not update HTMLPurifier.php, missing const $version.' . PHP_EOL; + exit; +} +file_put_contents('library/HTMLPurifier.php', $htmlpurifier_c); + +$config_c = file_get_contents('library/HTMLPurifier/Config.php'); +$config_c = preg_replace( + '/public \$version = \'.+?\';/', + "public \$version = '$version';", + $config_c, + 1, $c +); +if (!$c) { + echo 'Could not update Config.php, missing public $version.' . PHP_EOL; + exit; +} +file_put_contents('library/HTMLPurifier/Config.php', $config_c); + +passthru('php maintenance/flush.php'); + +if ($is_dev) echo "Review changes, write something in WHATSNEW and FOCUS, and then commit with log 'Release $version.'" . PHP_EOL; +else echo "Numbers updated to dev, no other modifications necessary!"; + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/release2-tag.php b/vendor/ezyang/htmlpurifier/release2-tag.php new file mode 100644 index 000000000..25e5300d8 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/release2-tag.php @@ -0,0 +1,22 @@ +<?php + +// Tags releases + +if (php_sapi_name() != 'cli') { + echo 'Release script cannot be called from web-browser.'; + exit; +} + +require 'svn.php'; + +$svn_info = my_svn_info('.'); + +$version = trim(file_get_contents('VERSION')); + +$trunk_url = $svn_info['Repository Root'] . '/htmlpurifier/trunk'; +$trunk_tag_url = $svn_info['Repository Root'] . '/htmlpurifier/tags/' . $version; + +echo "Tagging trunk to tags/$version..."; +passthru("svn copy --message \"Tag $version release.\" $trunk_url $trunk_tag_url"); + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/test-settings.sample.php b/vendor/ezyang/htmlpurifier/test-settings.sample.php new file mode 100644 index 000000000..480b66279 --- /dev/null +++ b/vendor/ezyang/htmlpurifier/test-settings.sample.php @@ -0,0 +1,74 @@ +<?php + +// ATTENTION! DO NOT EDIT THIS FILE! +// This file is necessary to run the unit tests and profiling scripts. +// Please copy it to 'test-settings.php' and make the necessary edits. + +// Note: The only external library you *need* is SimpleTest; everything else +// is optional. + +// We've got a lot of tests, so we recommend turning the limit off. +set_time_limit(0); + +// Turning off output buffering will prevent mysterious errors from core dumps. +$data = @ob_get_clean(); +if ($data !== false && $data !== '') { + echo "Output buffer contains data [".urlencode($data)."]\n"; + exit; +} + +// ----------------------------------------------------------------------------- +// REQUIRED SETTINGS + +// Note on running SimpleTest: +// You want the Git copy of SimpleTest, found here: +// https://github.com/simpletest/simpletest/ +// +// If SimpleTest is borked with HTML Purifier, please contact me or +// the SimpleTest devs; I am a developer for SimpleTest so I should be +// able to quickly assess a fix. SimpleTest's problem is my problem! + +// Where is SimpleTest located? Remember to include a trailing slash! +$simpletest_location = '/path/to/simpletest/'; + +// ----------------------------------------------------------------------------- +// OPTIONAL SETTINGS + +// Note on running PHPT: +// Vanilla PHPT from https://github.com/tswicegood/PHPT_Core should +// work fine on Linux w/o multitest. +// +// To do multitest or Windows testing, you'll need some more +// patches at https://github.com/ezyang/PHPT_Core +// +// I haven't tested the Windows setup in a while so I don't know if +// it still works. + +// Should PHPT tests be enabled? +$GLOBALS['HTMLPurifierTest']['PHPT'] = false; + +// If PHPT isn't in your Path via PEAR, set that here: +// set_include_path('/path/to/phpt/Core/src' . PATH_SEPARATOR . get_include_path()); + +// Where is CSSTidy located? (Include trailing slash. Leave false to disable.) +$csstidy_location = false; + +// For tests/multitest.php, which versions to test? +$versions_to_test = array(); + +// Stable PHP binary to use when invoking maintenance scripts. +$php = 'php'; + +// For tests/multitest.php, what is the multi-version executable? It must +// accept an extra parameter (version number) before all other arguments +$phpv = false; + +// Should PEAR tests be run? If you've got a valid PEAR installation, set this +// to true (or, if it's not in the include path, to its install directory). +$GLOBALS['HTMLPurifierTest']['PEAR'] = false; + +// If PEAR is enabled, what PEAR tests should be run? (Note: you will +// need to ensure these libraries are installed) +$GLOBALS['HTMLPurifierTest']['Net_IDNA2'] = true; + +// vim: et sw=4 sts=4 diff --git a/vendor/ezyang/htmlpurifier/test-settings.travis.php b/vendor/ezyang/htmlpurifier/test-settings.travis.php new file mode 100644 index 000000000..b1edce4aa --- /dev/null +++ b/vendor/ezyang/htmlpurifier/test-settings.travis.php @@ -0,0 +1,72 @@ +<?php + +// This file is the configuration for Travis testing. + +// Note: The only external library you *need* is SimpleTest; everything else +// is optional. + +// We've got a lot of tests, so we recommend turning the limit off. +set_time_limit(0); + +// Turning off output buffering will prevent mysterious errors from core dumps. +$data = @ob_get_clean(); +if ($data !== false && $data !== '') { + echo "Output buffer contains data [".urlencode($data)."]\n"; + exit; +} + +// ----------------------------------------------------------------------------- +// REQUIRED SETTINGS + +// Note on running SimpleTest: +// You want the Git copy of SimpleTest, found here: +// https://github.com/simpletest/simpletest/ +// +// If SimpleTest is borked with HTML Purifier, please contact me or +// the SimpleTest devs; I am a developer for SimpleTest so I should be +// able to quickly assess a fix. SimpleTest's problem is my problem! + +// Where is SimpleTest located? Remember to include a trailing slash! +$simpletest_location = dirname(__FILE__) . '/simpletest/'; + +// ----------------------------------------------------------------------------- +// OPTIONAL SETTINGS + +// Note on running PHPT: +// Vanilla PHPT from https://github.com/tswicegood/PHPT_Core should +// work fine on Linux w/o multitest. +// +// To do multitest or Windows testing, you'll need some more +// patches at https://github.com/ezyang/PHPT_Core +// +// I haven't tested the Windows setup in a while so I don't know if +// it still works. + +// Should PHPT tests be enabled? +$GLOBALS['HTMLPurifierTest']['PHPT'] = false; + +// If PHPT isn't in your Path via PEAR, set that here: +// set_include_path('/path/to/phpt/Core/src' . PATH_SEPARATOR . get_include_path()); + +// Where is CSSTidy located? (Include trailing slash. Leave false to disable.) +$csstidy_location = false; + +// For tests/multitest.php, which versions to test? +$versions_to_test = array(); + +// Stable PHP binary to use when invoking maintenance scripts. +$php = 'php'; + +// For tests/multitest.php, what is the multi-version executable? It must +// accept an extra parameter (version number) before all other arguments +$phpv = false; + +// Should PEAR tests be run? If you've got a valid PEAR installation, set this +// to true (or, if it's not in the include path, to its install directory). +$GLOBALS['HTMLPurifierTest']['PEAR'] = false; + +// If PEAR is enabled, what PEAR tests should be run? (Note: you will +// need to ensure these libraries are installed) +$GLOBALS['HTMLPurifierTest']['Net_IDNA2'] = true; + +// vim: et sw=4 sts=4 diff --git a/vendor/michelf/php-markdown/License.md b/vendor/michelf/php-markdown/License.md new file mode 100644 index 000000000..c16197b69 --- /dev/null +++ b/vendor/michelf/php-markdown/License.md @@ -0,0 +1,36 @@ +PHP Markdown Lib +Copyright (c) 2004-2016 Michel Fortin +<https://michelf.ca/> +All rights reserved. + +Based on Markdown +Copyright (c) 2003-2006 John Gruber +<https://daringfireball.net/> +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name "Markdown" nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +This software is provided by the copyright holders and contributors "as +is" and any express or implied warranties, including, but not limited +to, the implied warranties of merchantability and fitness for a +particular purpose are disclaimed. In no event shall the copyright owner +or contributors be liable for any direct, indirect, incidental, special, +exemplary, or consequential damages (including, but not limited to, +procurement of substitute goods or services; loss of use, data, or +profits; or business interruption) however caused and on any theory of +liability, whether in contract, strict liability, or tort (including +negligence or otherwise) arising in any way out of the use of this +software, even if advised of the possibility of such damage. diff --git a/vendor/michelf/php-markdown/Michelf/Markdown.inc.php b/vendor/michelf/php-markdown/Michelf/Markdown.inc.php new file mode 100644 index 000000000..e2bd3808e --- /dev/null +++ b/vendor/michelf/php-markdown/Michelf/Markdown.inc.php @@ -0,0 +1,10 @@ +<?php + +// Use this file if you cannot use class autoloading. It will include all the +// files needed for the Markdown parser. +// +// Take a look at the PSR-0-compatible class autoloading implementation +// in the Readme.php file if you want a simple autoloader setup. + +require_once dirname(__FILE__) . '/MarkdownInterface.php'; +require_once dirname(__FILE__) . '/Markdown.php'; diff --git a/vendor/michelf/php-markdown/Michelf/Markdown.php b/vendor/michelf/php-markdown/Michelf/Markdown.php new file mode 100644 index 000000000..c3eaf4464 --- /dev/null +++ b/vendor/michelf/php-markdown/Michelf/Markdown.php @@ -0,0 +1,1896 @@ +<?php +/** + * Markdown - A text-to-HTML conversion tool for web writers + * + * @package php-markdown + * @author Michel Fortin <michel.fortin@michelf.com> + * @copyright 2004-2016 Michel Fortin <https://michelf.com/projects/php-markdown/> + * @copyright (Original Markdown) 2004-2006 John Gruber <https://daringfireball.net/projects/markdown/> + */ + +namespace Michelf; + +/** + * Markdown Parser Class + */ +class Markdown implements MarkdownInterface { + /** + * Define the package version + * @var string + */ + const MARKDOWNLIB_VERSION = "1.7.0"; + + /** + * Simple function interface - Initialize the parser and return the result + * of its transform method. This will work fine for derived classes too. + * + * @api + * + * @param string $text + * @return string + */ + public static function defaultTransform($text) { + // Take parser class on which this function was called. + $parser_class = \get_called_class(); + + // Try to take parser from the static parser list + static $parser_list; + $parser =& $parser_list[$parser_class]; + + // Create the parser it not already set + if (!$parser) { + $parser = new $parser_class; + } + + // Transform text using parser. + return $parser->transform($text); + } + + /** + * Configuration variables + */ + + /** + * Change to ">" for HTML output. + * @var string + */ + public $empty_element_suffix = " />"; + + /** + * The width of indentation of the output markup + * @var int + */ + public $tab_width = 4; + + /** + * Change to `true` to disallow markup or entities. + * @var boolean + */ + public $no_markup = false; + public $no_entities = false; + + + /** + * Change to `true` to enable line breaks on \n without two trailling spaces + * @var boolean + */ + public $hard_wrap = false; + + /** + * Predefined URLs and titles for reference links and images. + * @var array + */ + public $predef_urls = array(); + public $predef_titles = array(); + + /** + * Optional filter function for URLs + * @var callable + */ + public $url_filter_func = null; + + /** + * Optional header id="" generation callback function. + * @var callable + */ + public $header_id_func = null; + + /** + * Optional function for converting code block content to HTML + * @var callable + */ + public $code_block_content_func = null; + + /** + * Optional function for converting code span content to HTML. + * @var callable + */ + public $code_span_content_func = null; + + /** + * Class attribute to toggle "enhanced ordered list" behaviour + * setting this to true will allow ordered lists to start from the index + * number that is defined first. + * + * For example: + * 2. List item two + * 3. List item three + * + * Becomes: + * <ol start="2"> + * <li>List item two</li> + * <li>List item three</li> + * </ol> + * + * @var bool + */ + public $enhanced_ordered_list = false; + + /** + * Parser implementation + */ + + /** + * Regex to match balanced [brackets]. + * Needed to insert a maximum bracked depth while converting to PHP. + * @var int + */ + protected $nested_brackets_depth = 6; + protected $nested_brackets_re; + + protected $nested_url_parenthesis_depth = 4; + protected $nested_url_parenthesis_re; + + /** + * Table of hash values for escaped characters: + * @var string + */ + protected $escape_chars = '\`*_{}[]()>#+-.!'; + protected $escape_chars_re; + + /** + * Constructor function. Initialize appropriate member variables. + * @return void + */ + public function __construct() { + $this->_initDetab(); + $this->prepareItalicsAndBold(); + + $this->nested_brackets_re = + str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth). + str_repeat('\])*', $this->nested_brackets_depth); + + $this->nested_url_parenthesis_re = + str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth). + str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth); + + $this->escape_chars_re = '['.preg_quote($this->escape_chars).']'; + + // Sort document, block, and span gamut in ascendent priority order. + asort($this->document_gamut); + asort($this->block_gamut); + asort($this->span_gamut); + } + + + /** + * Internal hashes used during transformation. + * @var array + */ + protected $urls = array(); + protected $titles = array(); + protected $html_hashes = array(); + + /** + * Status flag to avoid invalid nesting. + * @var boolean + */ + protected $in_anchor = false; + + /** + * Called before the transformation process starts to setup parser states. + * @return void + */ + protected function setup() { + // Clear global hashes. + $this->urls = $this->predef_urls; + $this->titles = $this->predef_titles; + $this->html_hashes = array(); + $this->in_anchor = false; + } + + /** + * Called after the transformation process to clear any variable which may + * be taking up memory unnecessarly. + * @return void + */ + protected function teardown() { + $this->urls = array(); + $this->titles = array(); + $this->html_hashes = array(); + } + + /** + * Main function. Performs some preprocessing on the input text and pass + * it through the document gamut. + * + * @api + * + * @param string $text + * @return string + */ + public function transform($text) { + $this->setup(); + + # Remove UTF-8 BOM and marker character in input, if present. + $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text); + + # Standardize line endings: + # DOS to Unix and Mac to Unix + $text = preg_replace('{\r\n?}', "\n", $text); + + # Make sure $text ends with a couple of newlines: + $text .= "\n\n"; + + # Convert all tabs to spaces. + $text = $this->detab($text); + + # Turn block-level HTML blocks into hash entries + $text = $this->hashHTMLBlocks($text); + + # Strip any lines consisting only of spaces and tabs. + # This makes subsequent regexen easier to write, because we can + # match consecutive blank lines with /\n+/ instead of something + # contorted like /[ ]*\n+/ . + $text = preg_replace('/^[ ]+$/m', '', $text); + + # Run document gamut methods. + foreach ($this->document_gamut as $method => $priority) { + $text = $this->$method($text); + } + + $this->teardown(); + + return $text . "\n"; + } + + /** + * Define the document gamut + * @var array + */ + protected $document_gamut = array( + // Strip link definitions, store in hashes. + "stripLinkDefinitions" => 20, + "runBasicBlockGamut" => 30, + ); + + /** + * Strips link definitions from text, stores the URLs and titles in + * hash references + * @param string $text + * @return string + */ + protected function stripLinkDefinitions($text) { + + $less_than_tab = $this->tab_width - 1; + + // Link defs are in the form: ^[id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + (?: + <(.+?)> # url = $2 + | + (\S+?) # url = $3 + ) + [ ]* + \n? # maybe one newline + [ ]* + (?: + (?<=\s) # lookbehind for whitespace + ["(] + (.*?) # title = $4 + [")] + [ ]* + )? # title is optional + (?:\n+|\Z) + }xm', + array($this, '_stripLinkDefinitions_callback'), + $text + ); + return $text; + } + + /** + * The callback to strip link definitions + * @param array $matches + * @return string + */ + protected function _stripLinkDefinitions_callback($matches) { + $link_id = strtolower($matches[1]); + $url = $matches[2] == '' ? $matches[3] : $matches[2]; + $this->urls[$link_id] = $url; + $this->titles[$link_id] =& $matches[4]; + return ''; // String that will replace the block + } + + /** + * Hashify HTML blocks + * @param string $text + * @return string + */ + protected function hashHTMLBlocks($text) { + if ($this->no_markup) { + return $text; + } + + $less_than_tab = $this->tab_width - 1; + + /** + * Hashify HTML blocks: + * + * We only want to do this for block-level HTML tags, such as headers, + * lists, and tables. That's because we still want to wrap <p>s around + * "paragraphs" that are wrapped in non-block-level tags, such as + * anchors, phrase emphasis, and spans. The list of tags we're looking + * for is hard-coded: + * + * * List "a" is made of tags which can be both inline or block-level. + * These will be treated block-level when the start tag is alone on + * its line, otherwise they're not matched here and will be taken as + * inline later. + * * List "b" is made of tags which are always block-level; + */ + $block_tags_a_re = 'ins|del'; + $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'. + 'script|noscript|style|form|fieldset|iframe|math|svg|'. + 'article|section|nav|aside|hgroup|header|footer|'. + 'figure'; + + // Regular expression for the content of a block tag. + $nested_tags_level = 4; + $attr = ' + (?> # optional tag attributes + \s # starts with whitespace + (?> + [^>"/]+ # text outside quotes + | + /+(?!>) # slash not followed by ">" + | + "[^"]*" # text inside double quotes (tolerate ">") + | + \'[^\']*\' # text inside single quotes (tolerate ">") + )* + )? + '; + $content = + str_repeat(' + (?> + [^<]+ # content without tag + | + <\2 # nested opening tag + '.$attr.' # attributes + (?> + /> + | + >', $nested_tags_level). // end of opening tag + '.*?'. // last level nested tag content + str_repeat(' + </\2\s*> # closing nested tag + ) + | + <(?!/\2\s*> # other tags with a different name + ) + )*', + $nested_tags_level); + $content2 = str_replace('\2', '\3', $content); + + /** + * First, look for nested blocks, e.g.: + * <div> + * <div> + * tags for inner block must be indented. + * </div> + * </div> + * + * The outermost tags must start at the left margin for this to match, + * and the inner nested divs must be indented. + * We need to do this before the next, more liberal match, because the + * next match will start at the first `<div>` and stop at the + * first `</div>`. + */ + $text = preg_replace_callback('{(?> + (?> + (?<=\n) # Starting on its own line + | # or + \A\n? # the at beginning of the doc + ) + ( # save in $1 + + # Match from `\n<tag>` to `</tag>\n`, handling nested tags + # in between. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_b_re.')# start tag = $2 + '.$attr.'> # attributes followed by > and \n + '.$content.' # content, support nesting + </\2> # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special version for tags of group a. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_a_re.')# start tag = $3 + '.$attr.'>[ ]*\n # attributes followed by > + '.$content2.' # content, support nesting + </\3> # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special case just for <hr />. It was easier to make a special + # case than to make the other regex more complicated. + + [ ]{0,'.$less_than_tab.'} + <(hr) # start tag = $2 + '.$attr.' # attributes + /?> # the matching end tag + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # Special case for standalone HTML comments: + + [ ]{0,'.$less_than_tab.'} + (?s: + <!-- .*? --> + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # PHP and ASP-style processor instructions (<? and <%) + + [ ]{0,'.$less_than_tab.'} + (?s: + <([?%]) # $2 + .*? + \2> + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + ) + )}Sxmi', + array($this, '_hashHTMLBlocks_callback'), + $text + ); + + return $text; + } + + /** + * The callback for hashing HTML blocks + * @param string $matches + * @return string + */ + protected function _hashHTMLBlocks_callback($matches) { + $text = $matches[1]; + $key = $this->hashBlock($text); + return "\n\n$key\n\n"; + } + + /** + * Called whenever a tag must be hashed when a function insert an atomic + * element in the text stream. Passing $text to through this function gives + * a unique text-token which will be reverted back when calling unhash. + * + * The $boundary argument specify what character should be used to surround + * the token. By convension, "B" is used for block elements that needs not + * to be wrapped into paragraph tags at the end, ":" is used for elements + * that are word separators and "X" is used in the general case. + * + * @param string $text + * @param string $boundary + * @return string + */ + protected function hashPart($text, $boundary = 'X') { + // Swap back any tag hash found in $text so we do not have to `unhash` + // multiple times at the end. + $text = $this->unhash($text); + + // Then hash the block. + static $i = 0; + $key = "$boundary\x1A" . ++$i . $boundary; + $this->html_hashes[$key] = $text; + return $key; // String that will replace the tag. + } + + /** + * Shortcut function for hashPart with block-level boundaries. + * @param string $text + * @return string + */ + protected function hashBlock($text) { + return $this->hashPart($text, 'B'); + } + + /** + * Define the block gamut - these are all the transformations that form + * block-level tags like paragraphs, headers, and list items. + * @var array + */ + protected $block_gamut = array( + "doHeaders" => 10, + "doHorizontalRules" => 20, + "doLists" => 40, + "doCodeBlocks" => 50, + "doBlockQuotes" => 60, + ); + + /** + * Run block gamut tranformations. + * + * We need to escape raw HTML in Markdown source before doing anything + * else. This need to be done for each block, and not only at the + * begining in the Markdown function since hashed blocks can be part of + * list items and could have been indented. Indented blocks would have + * been seen as a code block in a previous pass of hashHTMLBlocks. + * + * @param string $text + * @return string + */ + protected function runBlockGamut($text) { + $text = $this->hashHTMLBlocks($text); + return $this->runBasicBlockGamut($text); + } + + /** + * Run block gamut tranformations, without hashing HTML blocks. This is + * useful when HTML blocks are known to be already hashed, like in the first + * whole-document pass. + * + * @param string $text + * @return string + */ + protected function runBasicBlockGamut($text) { + + foreach ($this->block_gamut as $method => $priority) { + $text = $this->$method($text); + } + + // Finally form paragraph and restore hashed blocks. + $text = $this->formParagraphs($text); + + return $text; + } + + /** + * Convert horizontal rules + * @param string $text + * @return string + */ + protected function doHorizontalRules($text) { + return preg_replace( + '{ + ^[ ]{0,3} # Leading space + ([-*_]) # $1: First marker + (?> # Repeated marker group + [ ]{0,2} # Zero, one, or two spaces. + \1 # Marker character + ){2,} # Group repeated at least twice + [ ]* # Tailing spaces + $ # End of line. + }mx', + "\n".$this->hashBlock("<hr$this->empty_element_suffix")."\n", + $text + ); + } + + /** + * These are all the transformations that occur *within* block-level + * tags like paragraphs, headers, and list items. + * @var array + */ + protected $span_gamut = array( + // Process character escapes, code spans, and inline HTML + // in one shot. + "parseSpan" => -30, + // Process anchor and image tags. Images must come first, + // because ![foo][f] looks like an anchor. + "doImages" => 10, + "doAnchors" => 20, + // Make links out of things like `<https://example.com/>` + // Must come after doAnchors, because you can use < and > + // delimiters in inline links like [this](<url>). + "doAutoLinks" => 30, + "encodeAmpsAndAngles" => 40, + "doItalicsAndBold" => 50, + "doHardBreaks" => 60, + ); + + /** + * Run span gamut transformations + * @param string $text + * @return string + */ + protected function runSpanGamut($text) { + foreach ($this->span_gamut as $method => $priority) { + $text = $this->$method($text); + } + + return $text; + } + + /** + * Do hard breaks + * @param string $text + * @return string + */ + protected function doHardBreaks($text) { + if ($this->hard_wrap) { + return preg_replace_callback('/ *\n/', + array($this, '_doHardBreaks_callback'), $text); + } else { + return preg_replace_callback('/ {2,}\n/', + array($this, '_doHardBreaks_callback'), $text); + } + } + + /** + * Trigger part hashing for the hard break (callback method) + * @param array $matches + * @return string + */ + protected function _doHardBreaks_callback($matches) { + return $this->hashPart("<br$this->empty_element_suffix\n"); + } + + /** + * Turn Markdown link shortcuts into XHTML <a> tags. + * @param string $text + * @return string + */ + protected function doAnchors($text) { + if ($this->in_anchor) { + return $text; + } + $this->in_anchor = true; + + // First, handle reference-style links: [link text] [id] + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + ) + }xs', + array($this, '_doAnchors_reference_callback'), $text); + + // Next, inline-style links: [link text](url "optional title") + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + \( # literal paren + [ \n]* + (?: + <(.+?)> # href = $3 + | + ('.$this->nested_url_parenthesis_re.') # href = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # Title = $7 + \6 # matching quote + [ \n]* # ignore any spaces/tabs between closing quote and ) + )? # title is optional + \) + ) + }xs', + array($this, '_doAnchors_inline_callback'), $text); + + // Last, handle reference-style shortcuts: [link text] + // These must come last in case you've also got [link text][1] + // or [link text](/foo) + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ([^\[\]]+) # link text = $2; can\'t contain [ or ] + \] + ) + }xs', + array($this, '_doAnchors_reference_callback'), $text); + + $this->in_anchor = false; + return $text; + } + + /** + * Callback method to parse referenced anchors + * @param string $matches + * @return string + */ + protected function _doAnchors_reference_callback($matches) { + $whole_match = $matches[1]; + $link_text = $matches[2]; + $link_id =& $matches[3]; + + if ($link_id == "") { + // for shortcut links like [this][] or [this]. + $link_id = $link_text; + } + + // lower-case and turn embedded newlines into spaces + $link_id = strtolower($link_id); + $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); + + if (isset($this->urls[$link_id])) { + $url = $this->urls[$link_id]; + $url = $this->encodeURLAttribute($url); + + $result = "<a href=\"$url\""; + if ( isset( $this->titles[$link_id] ) ) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + $result = $this->hashPart($result); + } else { + $result = $whole_match; + } + return $result; + } + + /** + * Callback method to parse inline anchors + * @param string $matches + * @return string + */ + protected function _doAnchors_inline_callback($matches) { + $whole_match = $matches[1]; + $link_text = $this->runSpanGamut($matches[2]); + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + // If the URL was of the form <s p a c e s> it got caught by the HTML + // tag parser and hashed. Need to reverse the process before using + // the URL. + $unhashed = $this->unhash($url); + if ($unhashed != $url) + $url = preg_replace('/^<(.*)>$/', '\1', $unhashed); + + $url = $this->encodeURLAttribute($url); + + $result = "<a href=\"$url\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + + return $this->hashPart($result); + } + + /** + * Turn Markdown image shortcuts into <img> tags. + * @param string $text + * @return string + */ + protected function doImages($text) { + // First, handle reference-style labeled images: ![alt text][id] + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + + ) + }xs', + array($this, '_doImages_reference_callback'), $text); + + // Next, handle inline images: ![alt text](url "optional title") + // Don't forget: encode * and _ + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + \s? # One optional whitespace character + \( # literal paren + [ \n]* + (?: + <(\S*)> # src url = $3 + | + ('.$this->nested_url_parenthesis_re.') # src url = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # title = $7 + \6 # matching quote + [ \n]* + )? # title is optional + \) + ) + }xs', + array($this, '_doImages_inline_callback'), $text); + + return $text; + } + + /** + * Callback to parse references image tags + * @param array $matches + * @return string + */ + protected function _doImages_reference_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower($matches[3]); + + if ($link_id == "") { + $link_id = strtolower($alt_text); // for shortcut links like ![this][]. + } + + $alt_text = $this->encodeAttribute($alt_text); + if (isset($this->urls[$link_id])) { + $url = $this->encodeURLAttribute($this->urls[$link_id]); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($this->titles[$link_id])) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + $result .= $this->empty_element_suffix; + $result = $this->hashPart($result); + } else { + // If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; + } + + /** + * Callback to parse inline image tags + * @param array $matches + * @return string + */ + protected function _doImages_inline_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + $alt_text = $this->encodeAttribute($alt_text); + $url = $this->encodeURLAttribute($url); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; // $title already quoted + } + $result .= $this->empty_element_suffix; + + return $this->hashPart($result); + } + + /** + * Parse Markdown heading elements to HTML + * @param string $text + * @return string + */ + protected function doHeaders($text) { + /** + * Setext-style headers: + * Header 1 + * ======== + * + * Header 2 + * -------- + */ + $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', + array($this, '_doHeaders_callback_setext'), $text); + + /** + * atx-style headers: + * # Header 1 + * ## Header 2 + * ## Header 2 with closing hashes ## + * ... + * ###### Header 6 + */ + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + \n+ + }xm', + array($this, '_doHeaders_callback_atx'), $text); + + return $text; + } + + /** + * Setext header parsing callback + * @param array $matches + * @return string + */ + protected function _doHeaders_callback_setext($matches) { + // Terrible hack to check we haven't found an empty list item. + if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1])) { + return $matches[0]; + } + + $level = $matches[2]{0} == '=' ? 1 : 2; + + // ID attribute generation + $idAtt = $this->_generateIdFromHeaderValue($matches[1]); + + $block = "<h$level$idAtt>".$this->runSpanGamut($matches[1])."</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + /** + * ATX header parsing callback + * @param array $matches + * @return string + */ + protected function _doHeaders_callback_atx($matches) { + // ID attribute generation + $idAtt = $this->_generateIdFromHeaderValue($matches[2]); + + $level = strlen($matches[1]); + $block = "<h$level$idAtt>".$this->runSpanGamut($matches[2])."</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + /** + * If a header_id_func property is set, we can use it to automatically + * generate an id attribute. + * + * This method returns a string in the form id="foo", or an empty string + * otherwise. + * @param string $headerValue + * @return string + */ + protected function _generateIdFromHeaderValue($headerValue) { + if (!is_callable($this->header_id_func)) { + return ""; + } + + $idValue = call_user_func($this->header_id_func, $headerValue); + if (!$idValue) { + return ""; + } + + return ' id="' . $this->encodeAttribute($idValue) . '"'; + } + + /** + * Form HTML ordered (numbered) and unordered (bulleted) lists. + * @param string $text + * @return string + */ + protected function doLists($text) { + $less_than_tab = $this->tab_width - 1; + + // Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + + $markers_relist = array( + $marker_ul_re => $marker_ol_re, + $marker_ol_re => $marker_ul_re, + ); + + foreach ($markers_relist as $marker_re => $other_marker_re) { + // Re-usable pattern to match any entirel ul or ol list: + $whole_list_re = ' + ( # $1 = whole list + ( # $2 + ([ ]{0,'.$less_than_tab.'}) # $3 = number of spaces + ('.$marker_re.') # $4 = first list item marker + [ ]+ + ) + (?s:.+?) + ( # $5 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another list item marker + [ ]* + '.$marker_re.'[ ]+ + ) + | + (?= # Lookahead for another kind of list + \n + \3 # Must have the same indentation + '.$other_marker_re.'[ ]+ + ) + ) + ) + '; // mx + + // We use a different prefix before nested lists than top-level lists. + //See extended comment in _ProcessListItems(). + + if ($this->list_level) { + $text = preg_replace_callback('{ + ^ + '.$whole_list_re.' + }mx', + array($this, '_doLists_callback'), $text); + } else { + $text = preg_replace_callback('{ + (?:(?<=\n)\n|\A\n?) # Must eat the newline + '.$whole_list_re.' + }mx', + array($this, '_doLists_callback'), $text); + } + } + + return $text; + } + + /** + * List parsing callback + * @param array $matches + * @return string + */ + protected function _doLists_callback($matches) { + // Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + $marker_ol_start_re = '[0-9]+'; + + $list = $matches[1]; + $list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol"; + + $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re ); + + $list .= "\n"; + $result = $this->processListItems($list, $marker_any_re); + + $ol_start = 1; + if ($this->enhanced_ordered_list) { + // Get the start number for ordered list. + if ($list_type == 'ol') { + $ol_start_array = array(); + $ol_start_check = preg_match("/$marker_ol_start_re/", $matches[4], $ol_start_array); + if ($ol_start_check){ + $ol_start = $ol_start_array[0]; + } + } + } + + if ($ol_start > 1 && $list_type == 'ol'){ + $result = $this->hashBlock("<$list_type start=\"$ol_start\">\n" . $result . "</$list_type>"); + } else { + $result = $this->hashBlock("<$list_type>\n" . $result . "</$list_type>"); + } + return "\n". $result ."\n\n"; + } + + /** + * Nesting tracker for list levels + * @var integer + */ + protected $list_level = 0; + + /** + * Process the contents of a single ordered or unordered list, splitting it + * into individual list items. + * @param string $list_str + * @param string $marker_any_re + * @return string + */ + protected function processListItems($list_str, $marker_any_re) { + /** + * The $this->list_level global keeps track of when we're inside a list. + * Each time we enter a list, we increment it; when we leave a list, + * we decrement. If it's zero, we're not in a list anymore. + * + * We do this because when we're not inside a list, we want to treat + * something like this: + * + * I recommend upgrading to version + * 8. Oops, now this line is treated + * as a sub-list. + * + * As a single paragraph, despite the fact that the second line starts + * with a digit-period-space sequence. + * + * Whereas when we're inside a list (or sub-list), that line will be + * treated as the start of a sub-list. What a kludge, huh? This is + * an aspect of Markdown's syntax that's hard to parse perfectly + * without resorting to mind-reading. Perhaps the solution is to + * change the syntax rules such that sub-lists must start with a + * starting cardinal number; e.g. "1." or "a.". + */ + $this->list_level++; + + // Trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + $list_str = preg_replace_callback('{ + (\n)? # leading line = $1 + (^[ ]*) # leading whitespace = $2 + ('.$marker_any_re.' # list marker and space = $3 + (?:[ ]+|(?=\n)) # space only required if item is not empty + ) + ((?s:.*?)) # list item text = $4 + (?:(\n+(?=\n))|\n) # tailing blank line = $5 + (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n)))) + }xm', + array($this, '_processListItems_callback'), $list_str); + + $this->list_level--; + return $list_str; + } + + /** + * List item parsing callback + * @param array $matches + * @return string + */ + protected function _processListItems_callback($matches) { + $item = $matches[4]; + $leading_line =& $matches[1]; + $leading_space =& $matches[2]; + $marker_space = $matches[3]; + $tailing_blank_line =& $matches[5]; + + if ($leading_line || $tailing_blank_line || + preg_match('/\n{2,}/', $item)) + { + // Replace marker with the appropriate whitespace indentation + $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item; + $item = $this->runBlockGamut($this->outdent($item)."\n"); + } else { + // Recursion for sub-lists: + $item = $this->doLists($this->outdent($item)); + $item = $this->formParagraphs($item, false); + } + + return "<li>" . $item . "</li>\n"; + } + + /** + * Process Markdown `<pre><code>` blocks. + * @param string $text + * @return string + */ + protected function doCodeBlocks($text) { + $text = preg_replace_callback('{ + (?:\n\n|\A\n?) + ( # $1 = the code block -- one or more lines, starting with a space/tab + (?> + [ ]{'.$this->tab_width.'} # Lines must start with a tab or a tab-width of spaces + .*\n+ + )+ + ) + ((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z) # Lookahead for non-space at line-start, or end of doc + }xm', + array($this, '_doCodeBlocks_callback'), $text); + + return $text; + } + + /** + * Code block parsing callback + * @param array $matches + * @return string + */ + protected function _doCodeBlocks_callback($matches) { + $codeblock = $matches[1]; + + $codeblock = $this->outdent($codeblock); + if ($this->code_block_content_func) { + $codeblock = call_user_func($this->code_block_content_func, $codeblock, ""); + } else { + $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); + } + + # trim leading newlines and trailing newlines + $codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock); + + $codeblock = "<pre><code>$codeblock\n</code></pre>"; + return "\n\n" . $this->hashBlock($codeblock) . "\n\n"; + } + + /** + * Create a code span markup for $code. Called from handleSpanToken. + * @param string $code + * @return string + */ + protected function makeCodeSpan($code) { + if ($this->code_span_content_func) { + $code = call_user_func($this->code_span_content_func, $code); + } else { + $code = htmlspecialchars(trim($code), ENT_NOQUOTES); + } + return $this->hashPart("<code>$code</code>"); + } + + /** + * Define the emphasis operators with their regex matches + * @var array + */ + protected $em_relist = array( + '' => '(?:(?<!\*)\*(?!\*)|(?<!_)_(?!_))(?![\.,:;]?\s)', + '*' => '(?<![\s*])\*(?!\*)', + '_' => '(?<![\s_])_(?!_)', + ); + + /** + * Define the strong operators with their regex matches + * @var array + */ + protected $strong_relist = array( + '' => '(?:(?<!\*)\*\*(?!\*)|(?<!_)__(?!_))(?![\.,:;]?\s)', + '**' => '(?<![\s*])\*\*(?!\*)', + '__' => '(?<![\s_])__(?!_)', + ); + + /** + * Define the emphasis + strong operators with their regex matches + * @var array + */ + protected $em_strong_relist = array( + '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<!_)___(?!_))(?![\.,:;]?\s)', + '***' => '(?<![\s*])\*\*\*(?!\*)', + '___' => '(?<![\s_])___(?!_)', + ); + + /** + * Container for prepared regular expressions + * @var array + */ + protected $em_strong_prepared_relist; + + /** + * Prepare regular expressions for searching emphasis tokens in any + * context. + * @return void + */ + protected function prepareItalicsAndBold() { + foreach ($this->em_relist as $em => $em_re) { + foreach ($this->strong_relist as $strong => $strong_re) { + // Construct list of allowed token expressions. + $token_relist = array(); + if (isset($this->em_strong_relist["$em$strong"])) { + $token_relist[] = $this->em_strong_relist["$em$strong"]; + } + $token_relist[] = $em_re; + $token_relist[] = $strong_re; + + // Construct master expression from list. + $token_re = '{(' . implode('|', $token_relist) . ')}'; + $this->em_strong_prepared_relist["$em$strong"] = $token_re; + } + } + } + + /** + * Convert Markdown italics (emphasis) and bold (strong) to HTML + * @param string $text + * @return string + */ + protected function doItalicsAndBold($text) { + $token_stack = array(''); + $text_stack = array(''); + $em = ''; + $strong = ''; + $tree_char_em = false; + + while (1) { + // Get prepared regular expression for seraching emphasis tokens + // in current context. + $token_re = $this->em_strong_prepared_relist["$em$strong"]; + + // Each loop iteration search for the next emphasis token. + // Each token is then passed to handleSpanToken. + $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + $text_stack[0] .= $parts[0]; + $token =& $parts[1]; + $text =& $parts[2]; + + if (empty($token)) { + // Reached end of text span: empty stack without emitting. + // any more emphasis. + while ($token_stack[0]) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + } + break; + } + + $token_len = strlen($token); + if ($tree_char_em) { + // Reached closing marker while inside a three-char emphasis. + if ($token_len == 3) { + // Three-char closing marker, close em and strong. + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<strong><em>$span</em></strong>"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + $strong = ''; + } else { + // Other closing marker: close one em or strong and + // change current token state to match the other + $token_stack[0] = str_repeat($token{0}, 3-$token_len); + $tag = $token_len == 2 ? "strong" : "em"; + $span = $text_stack[0]; + $span = $this->runSpanGamut($span); + $span = "<$tag>$span</$tag>"; + $text_stack[0] = $this->hashPart($span); + $$tag = ''; // $$tag stands for $em or $strong + } + $tree_char_em = false; + } else if ($token_len == 3) { + if ($em) { + // Reached closing marker for both em and strong. + // Closing strong marker: + for ($i = 0; $i < 2; ++$i) { + $shifted_token = array_shift($token_stack); + $tag = strlen($shifted_token) == 2 ? "strong" : "em"; + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<$tag>$span</$tag>"; + $text_stack[0] .= $this->hashPart($span); + $$tag = ''; // $$tag stands for $em or $strong + } + } else { + // Reached opening three-char emphasis marker. Push on token + // stack; will be handled by the special condition above. + $em = $token{0}; + $strong = "$em$em"; + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $tree_char_em = true; + } + } else if ($token_len == 2) { + if ($strong) { + // Unwind any dangling emphasis marker: + if (strlen($token_stack[0]) == 1) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + } + // Closing strong marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<strong>$span</strong>"; + $text_stack[0] .= $this->hashPart($span); + $strong = ''; + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $strong = $token; + } + } else { + // Here $token_len == 1 + if ($em) { + if (strlen($token_stack[0]) == 1) { + // Closing emphasis marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<em>$span</em>"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + } else { + $text_stack[0] .= $token; + } + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $em = $token; + } + } + } + return $text_stack[0]; + } + + /** + * Parse Markdown blockquotes to HTML + * @param string $text + * @return string + */ + protected function doBlockQuotes($text) { + $text = preg_replace_callback('/ + ( # Wrap whole match in $1 + (?> + ^[ ]*>[ ]? # ">" at the start of a line + .+\n # rest of the first line + (.+\n)* # subsequent consecutive lines + \n* # blanks + )+ + ) + /xm', + array($this, '_doBlockQuotes_callback'), $text); + + return $text; + } + + /** + * Blockquote parsing callback + * @param array $matches + * @return string + */ + protected function _doBlockQuotes_callback($matches) { + $bq = $matches[1]; + // trim one level of quoting - trim whitespace-only lines + $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq); + $bq = $this->runBlockGamut($bq); // recurse + + $bq = preg_replace('/^/m', " ", $bq); + // These leading spaces cause problem with <pre> content, + // so we need to fix that: + $bq = preg_replace_callback('{(\s*<pre>.+?</pre>)}sx', + array($this, '_doBlockQuotes_callback2'), $bq); + + return "\n" . $this->hashBlock("<blockquote>\n$bq\n</blockquote>") . "\n\n"; + } + + /** + * Blockquote parsing callback + * @param array $matches + * @return string + */ + protected function _doBlockQuotes_callback2($matches) { + $pre = $matches[1]; + $pre = preg_replace('/^ /m', '', $pre); + return $pre; + } + + /** + * Parse paragraphs + * + * @param string $text String to process in paragraphs + * @param boolean $wrap_in_p Whether paragraphs should be wrapped in <p> tags + * @return string + */ + protected function formParagraphs($text, $wrap_in_p = true) { + // Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + // Wrap <p> tags and unhashify HTML blocks + foreach ($grafs as $key => $value) { + if (!preg_match('/^B\x1A[0-9]+B$/', $value)) { + // Is a paragraph. + $value = $this->runSpanGamut($value); + if ($wrap_in_p) { + $value = preg_replace('/^([ ]*)/', "<p>", $value); + $value .= "</p>"; + } + $grafs[$key] = $this->unhash($value); + } else { + // Is a block. + // Modify elements of @grafs in-place... + $graf = $value; + $block = $this->html_hashes[$graf]; + $graf = $block; +// if (preg_match('{ +// \A +// ( # $1 = <div> tag +// <div \s+ +// [^>]* +// \b +// markdown\s*=\s* ([\'"]) # $2 = attr quote char +// 1 +// \2 +// [^>]* +// > +// ) +// ( # $3 = contents +// .* +// ) +// (</div>) # $4 = closing tag +// \z +// }xs', $block, $matches)) +// { +// list(, $div_open, , $div_content, $div_close) = $matches; +// +// // We can't call Markdown(), because that resets the hash; +// // that initialization code should be pulled into its own sub, though. +// $div_content = $this->hashHTMLBlocks($div_content); +// +// // Run document gamut methods on the content. +// foreach ($this->document_gamut as $method => $priority) { +// $div_content = $this->$method($div_content); +// } +// +// $div_open = preg_replace( +// '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); +// +// $graf = $div_open . "\n" . $div_content . "\n" . $div_close; +// } + $grafs[$key] = $graf; + } + } + + return implode("\n\n", $grafs); + } + + /** + * Encode text for a double-quoted HTML attribute. This function + * is *not* suitable for attributes enclosed in single quotes. + * @param string $text + * @return string + */ + protected function encodeAttribute($text) { + $text = $this->encodeAmpsAndAngles($text); + $text = str_replace('"', '"', $text); + return $text; + } + + /** + * Encode text for a double-quoted HTML attribute containing a URL, + * applying the URL filter if set. Also generates the textual + * representation for the URL (removing mailto: or tel:) storing it in $text. + * This function is *not* suitable for attributes enclosed in single quotes. + * + * @param string $url + * @param string &$text Passed by reference + * @return string URL + */ + protected function encodeURLAttribute($url, &$text = null) { + if ($this->url_filter_func) { + $url = call_user_func($this->url_filter_func, $url); + } + + if (preg_match('{^mailto:}i', $url)) { + $url = $this->encodeEntityObfuscatedAttribute($url, $text, 7); + } else if (preg_match('{^tel:}i', $url)) { + $url = $this->encodeAttribute($url); + $text = substr($url, 4); + } else { + $url = $this->encodeAttribute($url); + $text = $url; + } + + return $url; + } + + /** + * Smart processing for ampersands and angle brackets that need to + * be encoded. Valid character entities are left alone unless the + * no-entities mode is set. + * @param string $text + * @return string + */ + protected function encodeAmpsAndAngles($text) { + if ($this->no_entities) { + $text = str_replace('&', '&', $text); + } else { + // Ampersand-encoding based entirely on Nat Irons's Amputator + // MT plugin: <http://bumppo.net/projects/amputator/> + $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', + '&', $text); + } + // Encode remaining <'s + $text = str_replace('<', '<', $text); + + return $text; + } + + /** + * Parse Markdown automatic links to anchor HTML tags + * @param string $text + * @return string + */ + protected function doAutoLinks($text) { + $text = preg_replace_callback('{<((https?|ftp|dict|tel):[^\'">\s]+)>}i', + array($this, '_doAutoLinks_url_callback'), $text); + + // Email addresses: <address@domain.foo> + $text = preg_replace_callback('{ + < + (?:mailto:)? + ( + (?: + [-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+ + | + ".*?" + ) + \@ + (?: + [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+ + | + \[[\d.a-fA-F:]+\] # IPv4 & IPv6 + ) + ) + > + }xi', + array($this, '_doAutoLinks_email_callback'), $text); + + return $text; + } + + /** + * Parse URL callback + * @param array $matches + * @return string + */ + protected function _doAutoLinks_url_callback($matches) { + $url = $this->encodeURLAttribute($matches[1], $text); + $link = "<a href=\"$url\">$text</a>"; + return $this->hashPart($link); + } + + /** + * Parse email address callback + * @param array $matches + * @return string + */ + protected function _doAutoLinks_email_callback($matches) { + $addr = $matches[1]; + $url = $this->encodeURLAttribute("mailto:$addr", $text); + $link = "<a href=\"$url\">$text</a>"; + return $this->hashPart($link); + } + + /** + * Input: some text to obfuscate, e.g. "mailto:foo@example.com" + * + * Output: the same text but with most characters encoded as either a + * decimal or hex entity, in the hopes of foiling most address + * harvesting spam bots. E.g.: + * + * mailto:foo + * @example.co + * m + * + * Note: the additional output $tail is assigned the same value as the + * ouput, minus the number of characters specified by $head_length. + * + * Based by a filter by Matthew Wickline, posted to BBEdit-Talk. + * With some optimizations by Milian Wolff. Forced encoding of HTML + * attribute special characters by Allan Odgaard. + * + * @param string $text + * @param string &$tail + * @param integer $head_length + * @return string + */ + protected function encodeEntityObfuscatedAttribute($text, &$tail = null, $head_length = 0) { + if ($text == "") { + return $tail = ""; + } + + $chars = preg_split('/(?<!^)(?!$)/', $text); + $seed = (int)abs(crc32($text) / strlen($text)); // Deterministic seed. + + foreach ($chars as $key => $char) { + $ord = ord($char); + // Ignore non-ascii chars. + if ($ord < 128) { + $r = ($seed * (1 + $key)) % 100; // Pseudo-random function. + // roughly 10% raw, 45% hex, 45% dec + // '@' *must* be encoded. I insist. + // '"' and '>' have to be encoded inside the attribute + if ($r > 90 && strpos('@"&>', $char) === false) { + /* do nothing */ + } else if ($r < 45) { + $chars[$key] = '&#x'.dechex($ord).';'; + } else { + $chars[$key] = '&#'.$ord.';'; + } + } + } + + $text = implode('', $chars); + $tail = $head_length ? implode('', array_slice($chars, $head_length)) : $text; + + return $text; + } + + /** + * Take the string $str and parse it into tokens, hashing embeded HTML, + * escaped characters and handling code spans. + * @param string $str + * @return string + */ + protected function parseSpan($str) { + $output = ''; + + $span_re = '{ + ( + \\\\'.$this->escape_chars_re.' + | + (?<![`\\\\]) + `+ # code span marker + '.( $this->no_markup ? '' : ' + | + <!-- .*? --> # comment + | + <\?.*?\?> | <%.*?%> # processing instruction + | + <[!$]?[-a-zA-Z0-9:_]+ # regular tags + (?> + \s + (?>[^"\'>]+|"[^"]*"|\'[^\']*\')* + )? + > + | + <[-a-zA-Z0-9:_]+\s*/> # xml-style empty tag + | + </[-a-zA-Z0-9:_]+\s*> # closing tag + ').' + ) + }xs'; + + while (1) { + // Each loop iteration seach for either the next tag, the next + // openning code span marker, or the next escaped character. + // Each token is then passed to handleSpanToken. + $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE); + + // Create token from text preceding tag. + if ($parts[0] != "") { + $output .= $parts[0]; + } + + // Check if we reach the end. + if (isset($parts[1])) { + $output .= $this->handleSpanToken($parts[1], $parts[2]); + $str = $parts[2]; + } else { + break; + } + } + + return $output; + } + + /** + * Handle $token provided by parseSpan by determining its nature and + * returning the corresponding value that should replace it. + * @param string $token + * @param string &$str + * @return string + */ + protected function handleSpanToken($token, &$str) { + switch ($token{0}) { + case "\\": + return $this->hashPart("&#". ord($token{1}). ";"); + case "`": + // Search for end marker in remaining text. + if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm', + $str, $matches)) + { + $str = $matches[2]; + $codespan = $this->makeCodeSpan($matches[1]); + return $this->hashPart($codespan); + } + return $token; // Return as text since no ending marker found. + default: + return $this->hashPart($token); + } + } + + /** + * Remove one level of line-leading tabs or spaces + * @param string $text + * @return string + */ + protected function outdent($text) { + return preg_replace('/^(\t|[ ]{1,' . $this->tab_width . '})/m', '', $text); + } + + + /** + * String length function for detab. `_initDetab` will create a function to + * handle UTF-8 if the default function does not exist. + * @var string + */ + protected $utf8_strlen = 'mb_strlen'; + + /** + * Replace tabs with the appropriate amount of spaces. + * + * For each line we separate the line in blocks delemited by tab characters. + * Then we reconstruct every line by adding the appropriate number of space + * between each blocks. + * + * @param string $text + * @return string + */ + protected function detab($text) { + $text = preg_replace_callback('/^.*\t.*$/m', + array($this, '_detab_callback'), $text); + + return $text; + } + + /** + * Replace tabs callback + * @param string $matches + * @return string + */ + protected function _detab_callback($matches) { + $line = $matches[0]; + $strlen = $this->utf8_strlen; // strlen function for UTF-8. + + // Split in blocks. + $blocks = explode("\t", $line); + // Add each blocks to the line. + $line = $blocks[0]; + unset($blocks[0]); // Do not add first block twice. + foreach ($blocks as $block) { + // Calculate amount of space, insert spaces, insert block. + $amount = $this->tab_width - + $strlen($line, 'UTF-8') % $this->tab_width; + $line .= str_repeat(" ", $amount) . $block; + } + return $line; + } + + /** + * Check for the availability of the function in the `utf8_strlen` property + * (initially `mb_strlen`). If the function is not available, create a + * function that will loosely count the number of UTF-8 characters with a + * regular expression. + * @return void + */ + protected function _initDetab() { + + if (function_exists($this->utf8_strlen)) { + return; + } + + $this->utf8_strlen = create_function('$text', 'return preg_match_all( + "/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/", + $text, $m);'); + } + + /** + * Swap back in all the tags hashed by _HashHTMLBlocks. + * @param string $text + * @return string + */ + protected function unhash($text) { + return preg_replace_callback('/(.)\x1A[0-9]+\1/', + array($this, '_unhash_callback'), $text); + } + + /** + * Unhashing callback + * @param array $matches + * @return string + */ + protected function _unhash_callback($matches) { + return $this->html_hashes[$matches[0]]; + } +} diff --git a/vendor/michelf/php-markdown/Michelf/MarkdownExtra.inc.php b/vendor/michelf/php-markdown/Michelf/MarkdownExtra.inc.php new file mode 100644 index 000000000..d09bd7a48 --- /dev/null +++ b/vendor/michelf/php-markdown/Michelf/MarkdownExtra.inc.php @@ -0,0 +1,11 @@ +<?php + +// Use this file if you cannot use class autoloading. It will include all the +// files needed for the MarkdownExtra parser. +// +// Take a look at the PSR-0-compatible class autoloading implementation +// in the Readme.php file if you want a simple autoloader setup. + +require_once dirname(__FILE__) . '/MarkdownInterface.php'; +require_once dirname(__FILE__) . '/Markdown.php'; +require_once dirname(__FILE__) . '/MarkdownExtra.php'; diff --git a/vendor/michelf/php-markdown/Michelf/MarkdownExtra.php b/vendor/michelf/php-markdown/Michelf/MarkdownExtra.php new file mode 100644 index 000000000..ac6b1b4f2 --- /dev/null +++ b/vendor/michelf/php-markdown/Michelf/MarkdownExtra.php @@ -0,0 +1,1785 @@ +<?php +/** + * Markdown Extra - A text-to-HTML conversion tool for web writers + * + * @package php-markdown + * @author Michel Fortin <michel.fortin@michelf.com> + * @copyright 2004-2016 Michel Fortin <https://michelf.com/projects/php-markdown/> + * @copyright (Original Markdown) 2004-2006 John Gruber <https://daringfireball.net/projects/markdown/> + */ + +namespace Michelf; + +/** + * Markdown Extra Parser Class + */ +class MarkdownExtra extends \Michelf\Markdown { + /** + * Configuration variables + */ + + /** + * Prefix for footnote ids. + * @var string + */ + public $fn_id_prefix = ""; + + /** + * Optional title attribute for footnote links and backlinks. + * @var string + */ + public $fn_link_title = ""; + public $fn_backlink_title = ""; + + /** + * Optional class attribute for footnote links and backlinks. + * @var string + */ + public $fn_link_class = "footnote-ref"; + public $fn_backlink_class = "footnote-backref"; + + /** + * Content to be displayed within footnote backlinks. The default is '↩'; + * the U+FE0E on the end is a Unicode variant selector used to prevent iOS + * from displaying the arrow character as an emoji. + * @var string + */ + public $fn_backlink_html = '↩︎'; + + /** + * Class name for table cell alignment (%% replaced left/center/right) + * For instance: 'go-%%' becomes 'go-left' or 'go-right' or 'go-center' + * If empty, the align attribute is used instead of a class name. + * @var string + */ + public $table_align_class_tmpl = ''; + + /** + * Optional class prefix for fenced code block. + * @var string + */ + public $code_class_prefix = ""; + + /** + * Class attribute for code blocks goes on the `code` tag; + * setting this to true will put attributes on the `pre` tag instead. + * @var boolean + */ + public $code_attr_on_pre = false; + + /** + * Predefined abbreviations. + * @var array + */ + public $predef_abbr = array(); + + /** + * Parser implementation + */ + + /** + * Constructor function. Initialize the parser object. + * @return void + */ + public function __construct() { + // Add extra escapable characters before parent constructor + // initialize the table. + $this->escape_chars .= ':|'; + + // Insert extra document, block, and span transformations. + // Parent constructor will do the sorting. + $this->document_gamut += array( + "doFencedCodeBlocks" => 5, + "stripFootnotes" => 15, + "stripAbbreviations" => 25, + "appendFootnotes" => 50, + ); + $this->block_gamut += array( + "doFencedCodeBlocks" => 5, + "doTables" => 15, + "doDefLists" => 45, + ); + $this->span_gamut += array( + "doFootnotes" => 5, + "doAbbreviations" => 70, + ); + + $this->enhanced_ordered_list = true; + parent::__construct(); + } + + + /** + * Extra variables used during extra transformations. + * @var array + */ + protected $footnotes = array(); + protected $footnotes_ordered = array(); + protected $footnotes_ref_count = array(); + protected $footnotes_numbers = array(); + protected $abbr_desciptions = array(); + /** @var @string */ + protected $abbr_word_re = ''; + + /** + * Give the current footnote number. + * @var integer + */ + protected $footnote_counter = 1; + + /** + * Setting up Extra-specific variables. + */ + protected function setup() { + parent::setup(); + + $this->footnotes = array(); + $this->footnotes_ordered = array(); + $this->footnotes_ref_count = array(); + $this->footnotes_numbers = array(); + $this->abbr_desciptions = array(); + $this->abbr_word_re = ''; + $this->footnote_counter = 1; + + foreach ($this->predef_abbr as $abbr_word => $abbr_desc) { + if ($this->abbr_word_re) + $this->abbr_word_re .= '|'; + $this->abbr_word_re .= preg_quote($abbr_word); + $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); + } + } + + /** + * Clearing Extra-specific variables. + */ + protected function teardown() { + $this->footnotes = array(); + $this->footnotes_ordered = array(); + $this->footnotes_ref_count = array(); + $this->footnotes_numbers = array(); + $this->abbr_desciptions = array(); + $this->abbr_word_re = ''; + + parent::teardown(); + } + + + /** + * Extra attribute parser + */ + + /** + * Expression to use to catch attributes (includes the braces) + * @var string + */ + protected $id_class_attr_catch_re = '\{((?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,})[ ]*\}'; + + /** + * Expression to use when parsing in a context when no capture is desired + * @var string + */ + protected $id_class_attr_nocatch_re = '\{(?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,}[ ]*\}'; + + /** + * Parse attributes caught by the $this->id_class_attr_catch_re expression + * and return the HTML-formatted list of attributes. + * + * Currently supported attributes are .class and #id. + * + * In addition, this method also supports supplying a default Id value, + * which will be used to populate the id attribute in case it was not + * overridden. + * @param string $tag_name + * @param string $attr + * @param mixed $defaultIdValue + * @param array $classes + * @return string + */ + protected function doExtraAttributes($tag_name, $attr, $defaultIdValue = null, $classes = array()) { + if (empty($attr) && !$defaultIdValue && empty($classes)) return ""; + + // Split on components + preg_match_all('/[#.a-z][-_:a-zA-Z0-9=]+/', $attr, $matches); + $elements = $matches[0]; + + // Handle classes and IDs (only first ID taken into account) + $attributes = array(); + $id = false; + foreach ($elements as $element) { + if ($element{0} == '.') { + $classes[] = substr($element, 1); + } else if ($element{0} == '#') { + if ($id === false) $id = substr($element, 1); + } else if (strpos($element, '=') > 0) { + $parts = explode('=', $element, 2); + $attributes[] = $parts[0] . '="' . $parts[1] . '"'; + } + } + + if (!$id) $id = $defaultIdValue; + + // Compose attributes as string + $attr_str = ""; + if (!empty($id)) { + $attr_str .= ' id="'.$this->encodeAttribute($id) .'"'; + } + if (!empty($classes)) { + $attr_str .= ' class="'. implode(" ", $classes) . '"'; + } + if (!$this->no_markup && !empty($attributes)) { + $attr_str .= ' '.implode(" ", $attributes); + } + return $attr_str; + } + + /** + * Strips link definitions from text, stores the URLs and titles in + * hash references. + * @param string $text + * @return string + */ + protected function stripLinkDefinitions($text) { + $less_than_tab = $this->tab_width - 1; + + // Link defs are in the form: ^[id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + (?: + <(.+?)> # url = $2 + | + (\S+?) # url = $3 + ) + [ ]* + \n? # maybe one newline + [ ]* + (?: + (?<=\s) # lookbehind for whitespace + ["(] + (.*?) # title = $4 + [")] + [ ]* + )? # title is optional + (?:[ ]* '.$this->id_class_attr_catch_re.' )? # $5 = extra id & class attr + (?:\n+|\Z) + }xm', + array($this, '_stripLinkDefinitions_callback'), + $text); + return $text; + } + + /** + * Strip link definition callback + * @param array $matches + * @return string + */ + protected function _stripLinkDefinitions_callback($matches) { + $link_id = strtolower($matches[1]); + $url = $matches[2] == '' ? $matches[3] : $matches[2]; + $this->urls[$link_id] = $url; + $this->titles[$link_id] =& $matches[4]; + $this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]); + return ''; // String that will replace the block + } + + + /** + * HTML block parser + */ + + /** + * Tags that are always treated as block tags + * @var string + */ + protected $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption|figure'; + + /** + * Tags treated as block tags only if the opening tag is alone on its line + * @var string + */ + protected $context_block_tags_re = 'script|noscript|style|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video'; + + /** + * Tags where markdown="1" default to span mode: + * @var string + */ + protected $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address'; + + /** + * Tags which must not have their contents modified, no matter where + * they appear + * @var string + */ + protected $clean_tags_re = 'script|style|math|svg'; + + /** + * Tags that do not need to be closed. + * @var string + */ + protected $auto_close_tags_re = 'hr|img|param|source|track'; + + /** + * Hashify HTML Blocks and "clean tags". + * + * We only want to do this for block-level HTML tags, such as headers, + * lists, and tables. That's because we still want to wrap <p>s around + * "paragraphs" that are wrapped in non-block-level tags, such as anchors, + * phrase emphasis, and spans. The list of tags we're looking for is + * hard-coded. + * + * This works by calling _HashHTMLBlocks_InMarkdown, which then calls + * _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1" + * attribute is found within a tag, _HashHTMLBlocks_InHTML calls back + * _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag. + * These two functions are calling each other. It's recursive! + * @param string $text + * @return string + */ + protected function hashHTMLBlocks($text) { + if ($this->no_markup) { + return $text; + } + + // Call the HTML-in-Markdown hasher. + list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text); + + return $text; + } + + /** + * Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags. + * + * * $indent is the number of space to be ignored when checking for code + * blocks. This is important because if we don't take the indent into + * account, something like this (which looks right) won't work as expected: + * + * <div> + * <div markdown="1"> + * Hello World. <-- Is this a Markdown code block or text? + * </div> <-- Is this a Markdown code block or a real tag? + * <div> + * + * If you don't like this, just don't indent the tag on which + * you apply the markdown="1" attribute. + * + * * If $enclosing_tag_re is not empty, stops at the first unmatched closing + * tag with that name. Nested tags supported. + * + * * If $span is true, text inside must treated as span. So any double + * newline will be replaced by a single newline so that it does not create + * paragraphs. + * + * Returns an array of that form: ( processed text , remaining text ) + * + * @param string $text + * @param integer $indent + * @param string $enclosing_tag_re + * @param boolean $span + * @return array + */ + protected function _hashHTMLBlocks_inMarkdown($text, $indent = 0, + $enclosing_tag_re = '', $span = false) + { + + if ($text === '') return array('', ''); + + // Regex to check for the presense of newlines around a block tag. + $newline_before_re = '/(?:^\n?|\n\n)*$/'; + $newline_after_re = + '{ + ^ # Start of text following the tag. + (?>[ ]*<!--.*?-->)? # Optional comment. + [ ]*\n # Must be followed by newline. + }xs'; + + // Regex to match any tag. + $block_tag_re = + '{ + ( # $2: Capture whole tag. + </? # Any opening or closing tag. + (?> # Tag name. + ' . $this->block_tags_re . ' | + ' . $this->context_block_tags_re . ' | + ' . $this->clean_tags_re . ' | + (?!\s)'.$enclosing_tag_re . ' + ) + (?: + (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name. + (?> + ".*?" | # Double quotes (can contain `>`) + \'.*?\' | # Single quotes (can contain `>`) + .+? # Anything but quotes and `>`. + )*? + )? + > # End of tag. + | + <!-- .*? --> # HTML Comment + | + <\?.*?\?> | <%.*?%> # Processing instruction + | + <!\[CDATA\[.*?\]\]> # CData Block + ' . ( !$span ? ' # If not in span. + | + # Indented code block + (?: ^[ ]*\n | ^ | \n[ ]*\n ) + [ ]{' . ($indent + 4) . '}[^\n]* \n + (?> + (?: [ ]{' . ($indent + 4) . '}[^\n]* | [ ]* ) \n + )* + | + # Fenced code block marker + (?<= ^ | \n ) + [ ]{0,' . ($indent + 3) . '}(?:~{3,}|`{3,}) + [ ]* + (?: \.?[-_:a-zA-Z0-9]+ )? # standalone class name + [ ]* + (?: ' . $this->id_class_attr_nocatch_re . ' )? # extra attributes + [ ]* + (?= \n ) + ' : '' ) . ' # End (if not is span). + | + # Code span marker + # Note, this regex needs to go after backtick fenced + # code blocks but it should also be kept outside of the + # "if not in span" condition adding backticks to the parser + `+ + ) + }xs'; + + + $depth = 0; // Current depth inside the tag tree. + $parsed = ""; // Parsed text that will be returned. + + // Loop through every tag until we find the closing tag of the parent + // or loop until reaching the end of text if no parent tag specified. + do { + // Split the text using the first $tag_match pattern found. + // Text before pattern will be first in the array, text after + // pattern will be at the end, and between will be any catches made + // by the pattern. + $parts = preg_split($block_tag_re, $text, 2, + PREG_SPLIT_DELIM_CAPTURE); + + // If in Markdown span mode, add a empty-string span-level hash + // after each newline to prevent triggering any block element. + if ($span) { + $void = $this->hashPart("", ':'); + $newline = "\n$void"; + $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void; + } + + $parsed .= $parts[0]; // Text before current tag. + + // If end of $text has been reached. Stop loop. + if (count($parts) < 3) { + $text = ""; + break; + } + + $tag = $parts[1]; // Tag to handle. + $text = $parts[2]; // Remaining text after current tag. + $tag_re = preg_quote($tag); // For use in a regular expression. + + // Check for: Fenced code block marker. + // Note: need to recheck the whole tag to disambiguate backtick + // fences from code spans + if (preg_match('{^\n?([ ]{0,' . ($indent + 3) . '})(~{3,}|`{3,})[ ]*(?:\.?[-_:a-zA-Z0-9]+)?[ ]*(?:' . $this->id_class_attr_nocatch_re . ')?[ ]*\n?$}', $tag, $capture)) { + // Fenced code block marker: find matching end marker. + $fence_indent = strlen($capture[1]); // use captured indent in re + $fence_re = $capture[2]; // use captured fence in re + if (preg_match('{^(?>.*\n)*?[ ]{' . ($fence_indent) . '}' . $fence_re . '[ ]*(?:\n|$)}', $text, + $matches)) + { + // End marker found: pass text unchanged until marker. + $parsed .= $tag . $matches[0]; + $text = substr($text, strlen($matches[0])); + } + else { + // No end marker: just skip it. + $parsed .= $tag; + } + } + // Check for: Indented code block. + else if ($tag{0} == "\n" || $tag{0} == " ") { + // Indented code block: pass it unchanged, will be handled + // later. + $parsed .= $tag; + } + // Check for: Code span marker + // Note: need to check this after backtick fenced code blocks + else if ($tag{0} == "`") { + // Find corresponding end marker. + $tag_re = preg_quote($tag); + if (preg_match('{^(?>.+?|\n(?!\n))*?(?<!`)' . $tag_re . '(?!`)}', + $text, $matches)) + { + // End marker found: pass text unchanged until marker. + $parsed .= $tag . $matches[0]; + $text = substr($text, strlen($matches[0])); + } + else { + // Unmatched marker: just skip it. + $parsed .= $tag; + } + } + // Check for: Opening Block level tag or + // Opening Context Block tag (like ins and del) + // used as a block tag (tag is alone on it's line). + else if (preg_match('{^<(?:' . $this->block_tags_re . ')\b}', $tag) || + ( preg_match('{^<(?:' . $this->context_block_tags_re . ')\b}', $tag) && + preg_match($newline_before_re, $parsed) && + preg_match($newline_after_re, $text) ) + ) + { + // Need to parse tag and following text using the HTML parser. + list($block_text, $text) = + $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true); + + // Make sure it stays outside of any paragraph by adding newlines. + $parsed .= "\n\n$block_text\n\n"; + } + // Check for: Clean tag (like script, math) + // HTML Comments, processing instructions. + else if (preg_match('{^<(?:' . $this->clean_tags_re . ')\b}', $tag) || + $tag{1} == '!' || $tag{1} == '?') + { + // Need to parse tag and following text using the HTML parser. + // (don't check for markdown attribute) + list($block_text, $text) = + $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false); + + $parsed .= $block_text; + } + // Check for: Tag with same name as enclosing tag. + else if ($enclosing_tag_re !== '' && + // Same name as enclosing tag. + preg_match('{^</?(?:' . $enclosing_tag_re . ')\b}', $tag)) + { + // Increase/decrease nested tag count. + if ($tag{1} == '/') $depth--; + else if ($tag{strlen($tag)-2} != '/') $depth++; + + if ($depth < 0) { + // Going out of parent element. Clean up and break so we + // return to the calling function. + $text = $tag . $text; + break; + } + + $parsed .= $tag; + } + else { + $parsed .= $tag; + } + } while ($depth >= 0); + + return array($parsed, $text); + } + + /** + * Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags. + * + * * Calls $hash_method to convert any blocks. + * * Stops when the first opening tag closes. + * * $md_attr indicate if the use of the `markdown="1"` attribute is allowed. + * (it is not inside clean tags) + * + * Returns an array of that form: ( processed text , remaining text ) + * @param string $text + * @param string $hash_method + * @param string $md_attr + * @return array + */ + protected function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) { + if ($text === '') return array('', ''); + + // Regex to match `markdown` attribute inside of a tag. + $markdown_attr_re = ' + { + \s* # Eat whitespace before the `markdown` attribute + markdown + \s*=\s* + (?> + (["\']) # $1: quote delimiter + (.*?) # $2: attribute value + \1 # matching delimiter + | + ([^\s>]*) # $3: unquoted attribute value + ) + () # $4: make $3 always defined (avoid warnings) + }xs'; + + // Regex to match any tag. + $tag_re = '{ + ( # $2: Capture whole tag. + </? # Any opening or closing tag. + [\w:$]+ # Tag name. + (?: + (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name. + (?> + ".*?" | # Double quotes (can contain `>`) + \'.*?\' | # Single quotes (can contain `>`) + .+? # Anything but quotes and `>`. + )*? + )? + > # End of tag. + | + <!-- .*? --> # HTML Comment + | + <\?.*?\?> | <%.*?%> # Processing instruction + | + <!\[CDATA\[.*?\]\]> # CData Block + ) + }xs'; + + $original_text = $text; // Save original text in case of faliure. + + $depth = 0; // Current depth inside the tag tree. + $block_text = ""; // Temporary text holder for current text. + $parsed = ""; // Parsed text that will be returned. + + // Get the name of the starting tag. + // (This pattern makes $base_tag_name_re safe without quoting.) + if (preg_match('/^<([\w:$]*)\b/', $text, $matches)) + $base_tag_name_re = $matches[1]; + + // Loop through every tag until we find the corresponding closing tag. + do { + // Split the text using the first $tag_match pattern found. + // Text before pattern will be first in the array, text after + // pattern will be at the end, and between will be any catches made + // by the pattern. + $parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + + if (count($parts) < 3) { + // End of $text reached with unbalenced tag(s). + // In that case, we return original text unchanged and pass the + // first character as filtered to prevent an infinite loop in the + // parent function. + return array($original_text{0}, substr($original_text, 1)); + } + + $block_text .= $parts[0]; // Text before current tag. + $tag = $parts[1]; // Tag to handle. + $text = $parts[2]; // Remaining text after current tag. + + // Check for: Auto-close tag (like <hr/>) + // Comments and Processing Instructions. + if (preg_match('{^</?(?:' . $this->auto_close_tags_re . ')\b}', $tag) || + $tag{1} == '!' || $tag{1} == '?') + { + // Just add the tag to the block as if it was text. + $block_text .= $tag; + } + else { + // Increase/decrease nested tag count. Only do so if + // the tag's name match base tag's. + if (preg_match('{^</?' . $base_tag_name_re . '\b}', $tag)) { + if ($tag{1} == '/') $depth--; + else if ($tag{strlen($tag)-2} != '/') $depth++; + } + + // Check for `markdown="1"` attribute and handle it. + if ($md_attr && + preg_match($markdown_attr_re, $tag, $attr_m) && + preg_match('/^1|block|span$/', $attr_m[2] . $attr_m[3])) + { + // Remove `markdown` attribute from opening tag. + $tag = preg_replace($markdown_attr_re, '', $tag); + + // Check if text inside this tag must be parsed in span mode. + $this->mode = $attr_m[2] . $attr_m[3]; + $span_mode = $this->mode == 'span' || $this->mode != 'block' && + preg_match('{^<(?:' . $this->contain_span_tags_re . ')\b}', $tag); + + // Calculate indent before tag. + if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) { + $strlen = $this->utf8_strlen; + $indent = $strlen($matches[1], 'UTF-8'); + } else { + $indent = 0; + } + + // End preceding block with this tag. + $block_text .= $tag; + $parsed .= $this->$hash_method($block_text); + + // Get enclosing tag name for the ParseMarkdown function. + // (This pattern makes $tag_name_re safe without quoting.) + preg_match('/^<([\w:$]*)\b/', $tag, $matches); + $tag_name_re = $matches[1]; + + // Parse the content using the HTML-in-Markdown parser. + list ($block_text, $text) + = $this->_hashHTMLBlocks_inMarkdown($text, $indent, + $tag_name_re, $span_mode); + + // Outdent markdown text. + if ($indent > 0) { + $block_text = preg_replace("/^[ ]{1,$indent}/m", "", + $block_text); + } + + // Append tag content to parsed text. + if (!$span_mode) $parsed .= "\n\n$block_text\n\n"; + else $parsed .= "$block_text"; + + // Start over with a new block. + $block_text = ""; + } + else $block_text .= $tag; + } + + } while ($depth > 0); + + // Hash last block text that wasn't processed inside the loop. + $parsed .= $this->$hash_method($block_text); + + return array($parsed, $text); + } + + /** + * Called whenever a tag must be hashed when a function inserts a "clean" tag + * in $text, it passes through this function and is automaticaly escaped, + * blocking invalid nested overlap. + * @param string $text + * @return string + */ + protected function hashClean($text) { + return $this->hashPart($text, 'C'); + } + + /** + * Turn Markdown link shortcuts into XHTML <a> tags. + * @param string $text + * @return string + */ + protected function doAnchors($text) { + if ($this->in_anchor) { + return $text; + } + $this->in_anchor = true; + + // First, handle reference-style links: [link text] [id] + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + (' . $this->nested_brackets_re . ') # link text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + ) + }xs', + array($this, '_doAnchors_reference_callback'), $text); + + // Next, inline-style links: [link text](url "optional title") + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + (' . $this->nested_brackets_re . ') # link text = $2 + \] + \( # literal paren + [ \n]* + (?: + <(.+?)> # href = $3 + | + (' . $this->nested_url_parenthesis_re . ') # href = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # Title = $7 + \6 # matching quote + [ \n]* # ignore any spaces/tabs between closing quote and ) + )? # title is optional + \) + (?:[ ]? ' . $this->id_class_attr_catch_re . ' )? # $8 = id/class attributes + ) + }xs', + array($this, '_doAnchors_inline_callback'), $text); + + // Last, handle reference-style shortcuts: [link text] + // These must come last in case you've also got [link text][1] + // or [link text](/foo) + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ([^\[\]]+) # link text = $2; can\'t contain [ or ] + \] + ) + }xs', + array($this, '_doAnchors_reference_callback'), $text); + + $this->in_anchor = false; + return $text; + } + + /** + * Callback for reference anchors + * @param array $matches + * @return string + */ + protected function _doAnchors_reference_callback($matches) { + $whole_match = $matches[1]; + $link_text = $matches[2]; + $link_id =& $matches[3]; + + if ($link_id == "") { + // for shortcut links like [this][] or [this]. + $link_id = $link_text; + } + + // lower-case and turn embedded newlines into spaces + $link_id = strtolower($link_id); + $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); + + if (isset($this->urls[$link_id])) { + $url = $this->urls[$link_id]; + $url = $this->encodeURLAttribute($url); + + $result = "<a href=\"$url\""; + if ( isset( $this->titles[$link_id] ) ) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + if (isset($this->ref_attr[$link_id])) + $result .= $this->ref_attr[$link_id]; + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + $result = $this->hashPart($result); + } + else { + $result = $whole_match; + } + return $result; + } + + /** + * Callback for inline anchors + * @param array $matches + * @return string + */ + protected function _doAnchors_inline_callback($matches) { + $whole_match = $matches[1]; + $link_text = $this->runSpanGamut($matches[2]); + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); + + // if the URL was of the form <s p a c e s> it got caught by the HTML + // tag parser and hashed. Need to reverse the process before using the URL. + $unhashed = $this->unhash($url); + if ($unhashed != $url) + $url = preg_replace('/^<(.*)>$/', '\1', $unhashed); + + $url = $this->encodeURLAttribute($url); + + $result = "<a href=\"$url\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + $result .= $attr; + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + + return $this->hashPart($result); + } + + /** + * Turn Markdown image shortcuts into <img> tags. + * @param string $text + * @return string + */ + protected function doImages($text) { + // First, handle reference-style labeled images: ![alt text][id] + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + (' . $this->nested_brackets_re . ') # alt text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + + ) + }xs', + array($this, '_doImages_reference_callback'), $text); + + // Next, handle inline images: ![alt text](url "optional title") + // Don't forget: encode * and _ + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + (' . $this->nested_brackets_re . ') # alt text = $2 + \] + \s? # One optional whitespace character + \( # literal paren + [ \n]* + (?: + <(\S*)> # src url = $3 + | + (' . $this->nested_url_parenthesis_re . ') # src url = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # title = $7 + \6 # matching quote + [ \n]* + )? # title is optional + \) + (?:[ ]? ' . $this->id_class_attr_catch_re . ' )? # $8 = id/class attributes + ) + }xs', + array($this, '_doImages_inline_callback'), $text); + + return $text; + } + + /** + * Callback for referenced images + * @param array $matches + * @return string + */ + protected function _doImages_reference_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower($matches[3]); + + if ($link_id == "") { + $link_id = strtolower($alt_text); // for shortcut links like ![this][]. + } + + $alt_text = $this->encodeAttribute($alt_text); + if (isset($this->urls[$link_id])) { + $url = $this->encodeURLAttribute($this->urls[$link_id]); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($this->titles[$link_id])) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + if (isset($this->ref_attr[$link_id])) + $result .= $this->ref_attr[$link_id]; + $result .= $this->empty_element_suffix; + $result = $this->hashPart($result); + } + else { + // If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; + } + + /** + * Callback for inline images + * @param array $matches + * @return string + */ + protected function _doImages_inline_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); + + $alt_text = $this->encodeAttribute($alt_text); + $url = $this->encodeURLAttribute($url); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; // $title already quoted + } + $result .= $attr; + $result .= $this->empty_element_suffix; + + return $this->hashPart($result); + } + + /** + * Process markdown headers. Redefined to add ID and class attribute support. + * @param string $text + * @return string + */ + protected function doHeaders($text) { + // Setext-style headers: + // Header 1 {#header1} + // ======== + // + // Header 2 {#header2 .class1 .class2} + // -------- + // + $text = preg_replace_callback( + '{ + (^.+?) # $1: Header text + (?:[ ]+ ' . $this->id_class_attr_catch_re . ' )? # $3 = id/class attributes + [ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer + }mx', + array($this, '_doHeaders_callback_setext'), $text); + + // atx-style headers: + // # Header 1 {#header1} + // ## Header 2 {#header2} + // ## Header 2 with closing hashes ## {#header3.class1.class2} + // ... + // ###### Header 6 {.class2} + // + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + (?:[ ]+ ' . $this->id_class_attr_catch_re . ' )? # $3 = id/class attributes + [ ]* + \n+ + }xm', + array($this, '_doHeaders_callback_atx'), $text); + + return $text; + } + + /** + * Callback for setext headers + * @param array $matches + * @return string + */ + protected function _doHeaders_callback_setext($matches) { + if ($matches[3] == '-' && preg_match('{^- }', $matches[1])) { + return $matches[0]; + } + + $level = $matches[3]{0} == '=' ? 1 : 2; + + $defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[1]) : null; + + $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2], $defaultId); + $block = "<h$level$attr>" . $this->runSpanGamut($matches[1]) . "</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + /** + * Callback for atx headers + * @param array $matches + * @return string + */ + protected function _doHeaders_callback_atx($matches) { + $level = strlen($matches[1]); + + $defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[2]) : null; + $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3], $defaultId); + $block = "<h$level$attr>" . $this->runSpanGamut($matches[2]) . "</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + /** + * Form HTML tables. + * @param string $text + * @return string + */ + protected function doTables($text) { + $less_than_tab = $this->tab_width - 1; + // Find tables with leading pipe. + // + // | Header 1 | Header 2 + // | -------- | -------- + // | Cell 1 | Cell 2 + // | Cell 3 | Cell 4 + $text = preg_replace_callback(' + { + ^ # Start of a line + [ ]{0,' . $less_than_tab . '} # Allowed whitespace. + [|] # Optional leading pipe (present) + (.+) \n # $1: Header row (at least one pipe) + + [ ]{0,' . $less_than_tab . '} # Allowed whitespace. + [|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline + + ( # $3: Cells + (?> + [ ]* # Allowed whitespace. + [|] .* \n # Row content. + )* + ) + (?=\n|\Z) # Stop at final double newline. + }xm', + array($this, '_doTable_leadingPipe_callback'), $text); + + // Find tables without leading pipe. + // + // Header 1 | Header 2 + // -------- | -------- + // Cell 1 | Cell 2 + // Cell 3 | Cell 4 + $text = preg_replace_callback(' + { + ^ # Start of a line + [ ]{0,' . $less_than_tab . '} # Allowed whitespace. + (\S.*[|].*) \n # $1: Header row (at least one pipe) + + [ ]{0,' . $less_than_tab . '} # Allowed whitespace. + ([-:]+[ ]*[|][-| :]*) \n # $2: Header underline + + ( # $3: Cells + (?> + .* [|] .* \n # Row content + )* + ) + (?=\n|\Z) # Stop at final double newline. + }xm', + array($this, '_DoTable_callback'), $text); + + return $text; + } + + /** + * Callback for removing the leading pipe for each row + * @param array $matches + * @return string + */ + protected function _doTable_leadingPipe_callback($matches) { + $head = $matches[1]; + $underline = $matches[2]; + $content = $matches[3]; + + $content = preg_replace('/^ *[|]/m', '', $content); + + return $this->_doTable_callback(array($matches[0], $head, $underline, $content)); + } + + /** + * Make the align attribute in a table + * @param string $alignname + * @return string + */ + protected function _doTable_makeAlignAttr($alignname) + { + if (empty($this->table_align_class_tmpl)) { + return " align=\"$alignname\""; + } + + $classname = str_replace('%%', $alignname, $this->table_align_class_tmpl); + return " class=\"$classname\""; + } + + /** + * Calback for processing tables + * @param array $matches + * @return string + */ + protected function _doTable_callback($matches) { + $head = $matches[1]; + $underline = $matches[2]; + $content = $matches[3]; + + // Remove any tailing pipes for each line. + $head = preg_replace('/[|] *$/m', '', $head); + $underline = preg_replace('/[|] *$/m', '', $underline); + $content = preg_replace('/[|] *$/m', '', $content); + + // Reading alignement from header underline. + $separators = preg_split('/ *[|] */', $underline); + foreach ($separators as $n => $s) { + if (preg_match('/^ *-+: *$/', $s)) + $attr[$n] = $this->_doTable_makeAlignAttr('right'); + else if (preg_match('/^ *:-+: *$/', $s)) + $attr[$n] = $this->_doTable_makeAlignAttr('center'); + else if (preg_match('/^ *:-+ *$/', $s)) + $attr[$n] = $this->_doTable_makeAlignAttr('left'); + else + $attr[$n] = ''; + } + + // Parsing span elements, including code spans, character escapes, + // and inline HTML tags, so that pipes inside those gets ignored. + $head = $this->parseSpan($head); + $headers = preg_split('/ *[|] */', $head); + $col_count = count($headers); + $attr = array_pad($attr, $col_count, ''); + + // Write column headers. + $text = "<table>\n"; + $text .= "<thead>\n"; + $text .= "<tr>\n"; + foreach ($headers as $n => $header) + $text .= " <th$attr[$n]>" . $this->runSpanGamut(trim($header)) . "</th>\n"; + $text .= "</tr>\n"; + $text .= "</thead>\n"; + + // Split content by row. + $rows = explode("\n", trim($content, "\n")); + + $text .= "<tbody>\n"; + foreach ($rows as $row) { + // Parsing span elements, including code spans, character escapes, + // and inline HTML tags, so that pipes inside those gets ignored. + $row = $this->parseSpan($row); + + // Split row by cell. + $row_cells = preg_split('/ *[|] */', $row, $col_count); + $row_cells = array_pad($row_cells, $col_count, ''); + + $text .= "<tr>\n"; + foreach ($row_cells as $n => $cell) + $text .= " <td$attr[$n]>" . $this->runSpanGamut(trim($cell)) . "</td>\n"; + $text .= "</tr>\n"; + } + $text .= "</tbody>\n"; + $text .= "</table>"; + + return $this->hashBlock($text) . "\n"; + } + + /** + * Form HTML definition lists. + * @param string $text + * @return string + */ + protected function doDefLists($text) { + $less_than_tab = $this->tab_width - 1; + + // Re-usable pattern to match any entire dl list: + $whole_list_re = '(?> + ( # $1 = whole list + ( # $2 + [ ]{0,' . $less_than_tab . '} + ((?>.*\S.*\n)+) # $3 = defined term + \n? + [ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition + ) + (?s:.+?) + ( # $4 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another term + [ ]{0,' . $less_than_tab . '} + (?: \S.*\n )+? # defined term + \n? + [ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition + ) + (?! # Negative lookahead for another definition + [ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition + ) + ) + ) + )'; // mx + + $text = preg_replace_callback('{ + (?>\A\n?|(?<=\n\n)) + ' . $whole_list_re . ' + }mx', + array($this, '_doDefLists_callback'), $text); + + return $text; + } + + /** + * Callback for processing definition lists + * @param array $matches + * @return string + */ + protected function _doDefLists_callback($matches) { + // Re-usable patterns to match list item bullets and number markers: + $list = $matches[1]; + + // Turn double returns into triple returns, so that we can make a + // paragraph for the last item in a list, if necessary: + $result = trim($this->processDefListItems($list)); + $result = "<dl>\n" . $result . "\n</dl>"; + return $this->hashBlock($result) . "\n\n"; + } + + /** + * Process the contents of a single definition list, splitting it + * into individual term and definition list items. + * @param string $list_str + * @return string + */ + protected function processDefListItems($list_str) { + + $less_than_tab = $this->tab_width - 1; + + // Trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + // Process definition terms. + $list_str = preg_replace_callback('{ + (?>\A\n?|\n\n+) # leading line + ( # definition terms = $1 + [ ]{0,' . $less_than_tab . '} # leading whitespace + (?!\:[ ]|[ ]) # negative lookahead for a definition + # mark (colon) or more whitespace. + (?> \S.* \n)+? # actual term (not whitespace). + ) + (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed + # with a definition mark. + }xm', + array($this, '_processDefListItems_callback_dt'), $list_str); + + // Process actual definitions. + $list_str = preg_replace_callback('{ + \n(\n+)? # leading line = $1 + ( # marker space = $2 + [ ]{0,' . $less_than_tab . '} # whitespace before colon + \:[ ]+ # definition mark (colon) + ) + ((?s:.+?)) # definition text = $3 + (?= \n+ # stop at next definition mark, + (?: # next term or end of text + [ ]{0,' . $less_than_tab . '} \:[ ] | + <dt> | \z + ) + ) + }xm', + array($this, '_processDefListItems_callback_dd'), $list_str); + + return $list_str; + } + + /** + * Callback for <dt> elements in definition lists + * @param array $matches + * @return string + */ + protected function _processDefListItems_callback_dt($matches) { + $terms = explode("\n", trim($matches[1])); + $text = ''; + foreach ($terms as $term) { + $term = $this->runSpanGamut(trim($term)); + $text .= "\n<dt>" . $term . "</dt>"; + } + return $text . "\n"; + } + + /** + * Callback for <dd> elements in definition lists + * @param array $matches + * @return string + */ + protected function _processDefListItems_callback_dd($matches) { + $leading_line = $matches[1]; + $marker_space = $matches[2]; + $def = $matches[3]; + + if ($leading_line || preg_match('/\n{2,}/', $def)) { + // Replace marker with the appropriate whitespace indentation + $def = str_repeat(' ', strlen($marker_space)) . $def; + $def = $this->runBlockGamut($this->outdent($def . "\n\n")); + $def = "\n". $def ."\n"; + } + else { + $def = rtrim($def); + $def = $this->runSpanGamut($this->outdent($def)); + } + + return "\n<dd>" . $def . "</dd>\n"; + } + + /** + * Adding the fenced code block syntax to regular Markdown: + * + * ~~~ + * Code block + * ~~~ + * + * @param string $text + * @return string + */ + protected function doFencedCodeBlocks($text) { + + $less_than_tab = $this->tab_width; + + $text = preg_replace_callback('{ + (?:\n|\A) + # 1: Opening marker + ( + (?:~{3,}|`{3,}) # 3 or more tildes/backticks. + ) + [ ]* + (?: + \.?([-_:a-zA-Z0-9]+) # 2: standalone class name + )? + [ ]* + (?: + ' . $this->id_class_attr_catch_re . ' # 3: Extra attributes + )? + [ ]* \n # Whitespace and newline following marker. + + # 4: Content + ( + (?> + (?!\1 [ ]* \n) # Not a closing marker. + .*\n+ + )+ + ) + + # Closing marker. + \1 [ ]* (?= \n ) + }xm', + array($this, '_doFencedCodeBlocks_callback'), $text); + + return $text; + } + + /** + * Callback to process fenced code blocks + * @param array $matches + * @return string + */ + protected function _doFencedCodeBlocks_callback($matches) { + $classname =& $matches[2]; + $attrs =& $matches[3]; + $codeblock = $matches[4]; + + if ($this->code_block_content_func) { + $codeblock = call_user_func($this->code_block_content_func, $codeblock, $classname); + } else { + $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); + } + + $codeblock = preg_replace_callback('/^\n+/', + array($this, '_doFencedCodeBlocks_newlines'), $codeblock); + + $classes = array(); + if ($classname != "") { + if ($classname{0} == '.') + $classname = substr($classname, 1); + $classes[] = $this->code_class_prefix . $classname; + } + $attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs, null, $classes); + $pre_attr_str = $this->code_attr_on_pre ? $attr_str : ''; + $code_attr_str = $this->code_attr_on_pre ? '' : $attr_str; + $codeblock = "<pre$pre_attr_str><code$code_attr_str>$codeblock</code></pre>"; + + return "\n\n".$this->hashBlock($codeblock)."\n\n"; + } + + /** + * Replace new lines in fenced code blocks + * @param array $matches + * @return string + */ + protected function _doFencedCodeBlocks_newlines($matches) { + return str_repeat("<br$this->empty_element_suffix", + strlen($matches[0])); + } + + /** + * Redefining emphasis markers so that emphasis by underscore does not + * work in the middle of a word. + * @var array + */ + protected $em_relist = array( + '' => '(?:(?<!\*)\*(?!\*)|(?<![a-zA-Z0-9_])_(?!_))(?![\.,:;]?\s)', + '*' => '(?<![\s*])\*(?!\*)', + '_' => '(?<![\s_])_(?![a-zA-Z0-9_])', + ); + protected $strong_relist = array( + '' => '(?:(?<!\*)\*\*(?!\*)|(?<![a-zA-Z0-9_])__(?!_))(?![\.,:;]?\s)', + '**' => '(?<![\s*])\*\*(?!\*)', + '__' => '(?<![\s_])__(?![a-zA-Z0-9_])', + ); + protected $em_strong_relist = array( + '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<![a-zA-Z0-9_])___(?!_))(?![\.,:;]?\s)', + '***' => '(?<![\s*])\*\*\*(?!\*)', + '___' => '(?<![\s_])___(?![a-zA-Z0-9_])', + ); + + /** + * Parse text into paragraphs + * @param string $text String to process in paragraphs + * @param boolean $wrap_in_p Whether paragraphs should be wrapped in <p> tags + * @return string HTML output + */ + protected function formParagraphs($text, $wrap_in_p = true) { + // Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + // Wrap <p> tags and unhashify HTML blocks + foreach ($grafs as $key => $value) { + $value = trim($this->runSpanGamut($value)); + + // Check if this should be enclosed in a paragraph. + // Clean tag hashes & block tag hashes are left alone. + $is_p = $wrap_in_p && !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value); + + if ($is_p) { + $value = "<p>$value</p>"; + } + $grafs[$key] = $value; + } + + // Join grafs in one text, then unhash HTML tags. + $text = implode("\n\n", $grafs); + + // Finish by removing any tag hashes still present in $text. + $text = $this->unhash($text); + + return $text; + } + + + /** + * Footnotes - Strips link definitions from text, stores the URLs and + * titles in hash references. + * @param string $text + * @return string + */ + protected function stripFootnotes($text) { + $less_than_tab = $this->tab_width - 1; + + // Link defs are in the form: [^id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,' . $less_than_tab . '}\[\^(.+?)\][ ]?: # note_id = $1 + [ ]* + \n? # maybe *one* newline + ( # text = $2 (no blank lines allowed) + (?: + .+ # actual text + | + \n # newlines but + (?!\[.+?\][ ]?:\s)# negative lookahead for footnote or link definition marker. + (?!\n+[ ]{0,3}\S)# ensure line is not blank and followed + # by non-indented content + )* + ) + }xm', + array($this, '_stripFootnotes_callback'), + $text); + return $text; + } + + /** + * Callback for stripping footnotes + * @param array $matches + * @return string + */ + protected function _stripFootnotes_callback($matches) { + $note_id = $this->fn_id_prefix . $matches[1]; + $this->footnotes[$note_id] = $this->outdent($matches[2]); + return ''; // String that will replace the block + } + + /** + * Replace footnote references in $text [^id] with a special text-token + * which will be replaced by the actual footnote marker in appendFootnotes. + * @param string $text + * @return string + */ + protected function doFootnotes($text) { + if (!$this->in_anchor) { + $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text); + } + return $text; + } + + /** + * Append footnote list to text + * @param string $text + * @return string + */ + protected function appendFootnotes($text) { + $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', + array($this, '_appendFootnotes_callback'), $text); + + if (!empty($this->footnotes_ordered)) { + $text .= "\n\n"; + $text .= "<div class=\"footnotes\">\n"; + $text .= "<hr" . $this->empty_element_suffix . "\n"; + $text .= "<ol>\n\n"; + + $attr = ""; + if ($this->fn_backlink_class != "") { + $class = $this->fn_backlink_class; + $class = $this->encodeAttribute($class); + $attr .= " class=\"$class\""; + } + if ($this->fn_backlink_title != "") { + $title = $this->fn_backlink_title; + $title = $this->encodeAttribute($title); + $attr .= " title=\"$title\""; + } + $backlink_text = $this->fn_backlink_html; + $num = 0; + + while (!empty($this->footnotes_ordered)) { + $footnote = reset($this->footnotes_ordered); + $note_id = key($this->footnotes_ordered); + unset($this->footnotes_ordered[$note_id]); + $ref_count = $this->footnotes_ref_count[$note_id]; + unset($this->footnotes_ref_count[$note_id]); + unset($this->footnotes[$note_id]); + + $footnote .= "\n"; // Need to append newline before parsing. + $footnote = $this->runBlockGamut("$footnote\n"); + $footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', + array($this, '_appendFootnotes_callback'), $footnote); + + $attr = str_replace("%%", ++$num, $attr); + $note_id = $this->encodeAttribute($note_id); + + // Prepare backlink, multiple backlinks if multiple references + $backlink = "<a href=\"#fnref:$note_id\"$attr>$backlink_text</a>"; + for ($ref_num = 2; $ref_num <= $ref_count; ++$ref_num) { + $backlink .= " <a href=\"#fnref$ref_num:$note_id\"$attr>$backlink_text</a>"; + } + // Add backlink to last paragraph; create new paragraph if needed. + if (preg_match('{</p>$}', $footnote)) { + $footnote = substr($footnote, 0, -4) . " $backlink</p>"; + } else { + $footnote .= "\n\n<p>$backlink</p>"; + } + + $text .= "<li id=\"fn:$note_id\">\n"; + $text .= $footnote . "\n"; + $text .= "</li>\n\n"; + } + + $text .= "</ol>\n"; + $text .= "</div>"; + } + return $text; + } + + /** + * Callback for appending footnotes + * @param array $matches + * @return string + */ + protected function _appendFootnotes_callback($matches) { + $node_id = $this->fn_id_prefix . $matches[1]; + + // Create footnote marker only if it has a corresponding footnote *and* + // the footnote hasn't been used by another marker. + if (isset($this->footnotes[$node_id])) { + $num =& $this->footnotes_numbers[$node_id]; + if (!isset($num)) { + // Transfer footnote content to the ordered list and give it its + // number + $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id]; + $this->footnotes_ref_count[$node_id] = 1; + $num = $this->footnote_counter++; + $ref_count_mark = ''; + } else { + $ref_count_mark = $this->footnotes_ref_count[$node_id] += 1; + } + + $attr = ""; + if ($this->fn_link_class != "") { + $class = $this->fn_link_class; + $class = $this->encodeAttribute($class); + $attr .= " class=\"$class\""; + } + if ($this->fn_link_title != "") { + $title = $this->fn_link_title; + $title = $this->encodeAttribute($title); + $attr .= " title=\"$title\""; + } + + $attr = str_replace("%%", $num, $attr); + $node_id = $this->encodeAttribute($node_id); + + return + "<sup id=\"fnref$ref_count_mark:$node_id\">". + "<a href=\"#fn:$node_id\"$attr>$num</a>". + "</sup>"; + } + + return "[^" . $matches[1] . "]"; + } + + + /** + * Abbreviations - strips abbreviations from text, stores titles in hash + * references. + * @param string $text + * @return string + */ + protected function stripAbbreviations($text) { + $less_than_tab = $this->tab_width - 1; + + // Link defs are in the form: [id]*: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,' . $less_than_tab . '}\*\[(.+?)\][ ]?: # abbr_id = $1 + (.*) # text = $2 (no blank lines allowed) + }xm', + array($this, '_stripAbbreviations_callback'), + $text); + return $text; + } + + /** + * Callback for stripping abbreviations + * @param array $matches + * @return string + */ + protected function _stripAbbreviations_callback($matches) { + $abbr_word = $matches[1]; + $abbr_desc = $matches[2]; + if ($this->abbr_word_re) { + $this->abbr_word_re .= '|'; + } + $this->abbr_word_re .= preg_quote($abbr_word); + $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); + return ''; // String that will replace the block + } + + /** + * Find defined abbreviations in text and wrap them in <abbr> elements. + * @param string $text + * @return string + */ + protected function doAbbreviations($text) { + if ($this->abbr_word_re) { + // cannot use the /x modifier because abbr_word_re may + // contain significant spaces: + $text = preg_replace_callback('{' . + '(?<![\w\x1A])' . + '(?:' . $this->abbr_word_re . ')' . + '(?![\w\x1A])' . + '}', + array($this, '_doAbbreviations_callback'), $text); + } + return $text; + } + + /** + * Callback for processing abbreviations + * @param array $matches + * @return string + */ + protected function _doAbbreviations_callback($matches) { + $abbr = $matches[0]; + if (isset($this->abbr_desciptions[$abbr])) { + $desc = $this->abbr_desciptions[$abbr]; + if (empty($desc)) { + return $this->hashPart("<abbr>$abbr</abbr>"); + } else { + $desc = $this->encodeAttribute($desc); + return $this->hashPart("<abbr title=\"$desc\">$abbr</abbr>"); + } + } else { + return $matches[0]; + } + } +} diff --git a/vendor/michelf/php-markdown/Michelf/MarkdownInterface.inc.php b/vendor/michelf/php-markdown/Michelf/MarkdownInterface.inc.php new file mode 100644 index 000000000..c4e9ac7f6 --- /dev/null +++ b/vendor/michelf/php-markdown/Michelf/MarkdownInterface.inc.php @@ -0,0 +1,9 @@ +<?php + +// Use this file if you cannot use class autoloading. It will include all the +// files needed for the MarkdownInterface interface. +// +// Take a look at the PSR-0-compatible class autoloading implementation +// in the Readme.php file if you want a simple autoloader setup. + +require_once dirname(__FILE__) . '/MarkdownInterface.php'; diff --git a/vendor/michelf/php-markdown/Michelf/MarkdownInterface.php b/vendor/michelf/php-markdown/Michelf/MarkdownInterface.php new file mode 100644 index 000000000..8512e3753 --- /dev/null +++ b/vendor/michelf/php-markdown/Michelf/MarkdownInterface.php @@ -0,0 +1,38 @@ +<?php +/** + * Markdown - A text-to-HTML conversion tool for web writers + * + * @package php-markdown + * @author Michel Fortin <michel.fortin@michelf.com> + * @copyright 2004-2016 Michel Fortin <https://michelf.com/projects/php-markdown/> + * @copyright (Original Markdown) 2004-2006 John Gruber <https://daringfireball.net/projects/markdown/> + */ + +namespace Michelf; + +/** + * Markdown Parser Interface + */ +interface MarkdownInterface { + /** + * Initialize the parser and return the result of its transform method. + * This will work fine for derived classes too. + * + * @api + * + * @param string $text + * @return string + */ + public static function defaultTransform($text); + + /** + * Main function. Performs some preprocessing on the input text + * and pass it through the document gamut. + * + * @api + * + * @param string $text + * @return string + */ + public function transform($text); +} diff --git a/vendor/michelf/php-markdown/Readme.md b/vendor/michelf/php-markdown/Readme.md new file mode 100644 index 000000000..044407106 --- /dev/null +++ b/vendor/michelf/php-markdown/Readme.md @@ -0,0 +1,384 @@ +PHP Markdown +============ + +PHP Markdown Lib 1.7.0 - 29 Oct 2016 + +by Michel Fortin +<https://michelf.ca/> + +based on Markdown by John Gruber +<https://daringfireball.net/> + + +Introduction +------------ + +This is a library package that includes the PHP Markdown parser and its +sibling PHP Markdown Extra with additional features. + +Markdown is a text-to-HTML conversion tool for web writers. Markdown +allows you to write using an easy-to-read, easy-to-write plain text +format, then convert it to structurally valid XHTML (or HTML). + +"Markdown" is actually two things: a plain text markup syntax, and a +software tool, originally written in Perl, that converts the plain text +markup to HTML. PHP Markdown is a port to PHP of the original Markdown +program by John Gruber. + +* [Full documentation of the Markdown syntax](<https://daringfireball.net/projects/markdown/>) + — Daring Fireball (John Gruber) +* [Markdown Extra syntax additions](<https://michelf.ca/projects/php-markdown/extra/>) + — Michel Fortin + + +Requirement +----------- + +This library package requires PHP 5.3 or later. + +Note: The older plugin/library hybrid package for PHP Markdown and +PHP Markdown Extra is still maintained and will work with PHP 4.0.5 and later. + +Before PHP 5.3.7, pcre.backtrack_limit defaults to 100 000, which is too small +in many situations. You might need to set it to higher values. Later PHP +releases defaults to 1 000 000, which is usually fine. + + +Usage +----- + +This library package is meant to be used with class autoloading. For autoloading +to work, your project needs have setup a PSR-0-compatible autoloader. See the +included Readme.php file for a minimal autoloader setup. (If you cannot use +autoloading, see below.) + +With class autoloading in place, putting the 'Michelf' folder in your +include path should be enough for this to work: + + use \Michelf\Markdown; + $my_html = Markdown::defaultTransform($my_text); + +Markdown Extra syntax is also available the same way: + + use \Michelf\MarkdownExtra; + $my_html = MarkdownExtra::defaultTransform($my_text); + +If you wish to use PHP Markdown with another text filter function +built to parse HTML, you should filter the text *after* the `transform` +function call. This is an example with [PHP SmartyPants][psp]: + + use \Michelf\Markdown, \Michelf\SmartyPants; + $my_html = Markdown::defaultTransform($my_text); + $my_html = SmartyPants::defaultTransform($my_html); + +All these examples are using the static `defaultTransform` static function +found inside the parser class. If you want to customize the parser +configuration, you can also instantiate it directly and change some +configuration variables: + + use \Michelf\MarkdownExtra; + $parser = new MarkdownExtra; + $parser->fn_id_prefix = "post22-"; + $my_html = $parser->transform($my_text); + +To learn more, see the full list of [configuration variables]. + + [configuration variables]: https://michelf.ca/projects/php-markdown/configuration/ + + +### Usage without an autoloader + +If you cannot use class autoloading, you can still use `include` or `require` +to access the parser. To load the `\Michelf\Markdown` parser, do it this way: + + require_once 'Michelf/Markdown.inc.php'; + +Or, if you need the `\Michelf\MarkdownExtra` parser: + + require_once 'Michelf/MarkdownExtra.inc.php'; + +While the plain `.php` files depend on autoloading to work correctly, using the +`.inc.php` files instead will eagerly load the dependencies that would be +loaded on demand if you were using autoloading. + + +Public API and Versioning Policy +--------------------------------- + +Version numbers are of the form *major*.*minor*.*patch*. + +The public API of PHP Markdown consist of the two parser classes `Markdown` +and `MarkdownExtra`, their constructors, the `transform` and `defaultTransform` +functions and their configuration variables. The public API is stable for +a given major version number. It might get additions when the minor version +number increments. + +**Protected members are not considered public API.** This is unconventional +and deserves an explanation. Incrementing the major version number every time +the underlying implementation of something changes is going to give +nonessential version numbers for the vast majority of people who just use the +parser. Protected members are meant to create parser subclasses that behave in +different ways. Very few people create parser subclasses. I don't want to +discourage it by making everything private, but at the same time I can't +guarantee any stable hook between versions if you use protected members. + +**Syntax changes** will increment the minor number for new features, and the +patch number for small corrections. A *new feature* is something that needs a +change in the syntax documentation. Note that since PHP Markdown Lib includes +two parsers, a syntax change for either of them will increment the minor +number. Also note that there is nothing perfectly backward-compatible with the +Markdown syntax: all inputs are always valid, so new features always replace +something that was previously legal, although generally nonsensical to do. + + +Bugs +---- + +To file bug reports please send email to: +<michel.fortin@michelf.ca> + +Please include with your report: (1) the example input; (2) the output you +expected; (3) the output PHP Markdown actually produced. + +If you have a problem where Markdown gives you an empty result, first check +that the backtrack limit is not too low by running `php --info | grep pcre`. +See Installation and Requirement above for details. + + +Development and Testing +----------------------- + +Pull requests for fixing bugs are welcome. Proposed new features are +going to be meticulously reviewed -- taking into account backward compatibility, +potential side effects, and future extensibility -- before deciding on +acceptance or rejection. + +If you make a pull request that includes changes to the parser please add +tests for what is being changed to [MDTest][] and make a pull request there +too. + + [MDTest]: https://github.com/michelf/mdtest/ + + +Donations +--------- + +If you wish to make a donation that will help me devote more time to +PHP Markdown, please visit [michelf.ca/donate] or send Bitcoin to +[1HiuX34czvVPPdhXbUAsAu7pZcesniDCGH]. + + [michelf.ca/donate]: https://michelf.ca/donate/#!Thanks%20for%20PHP%20Markdown + [1HiuX34czvVPPdhXbUAsAu7pZcesniDCGH]: bitcoin:1HiuX34czvVPPdhXbUAsAu7pZcesniDCGH + + +Version History +--------------- + +PHP Markdown Lib 1.7.0 (29 Oct 2016) + +* Added a `hard_wrap` configuration variable to make all newline characters + in the text become `<br>` tags in the HTML output. By default, according + to the standard Markdown syntax these newlines are ignored unless they a + preceded by two spaces. Thanks to Jonathan Cohlmeyer for the implementation. + +* Improved the parsing of list items to fix problematic cases that came to + light with the addition of `hard_wrap`. This should have no effect on the + output except span-level list items that ended with two spaces (and thus + ended with a line break). + +* Added a `code_span_content_func` configuration variable which takes a + function that will convert the content of the code span to HTML. This can + be useful to implement syntax highlighting. Although contrary to its + code block equivalent, there is no syntax for specifying a language. + Credits to styxit for the implementation. + +* Fixed a Markdwon Extra issue where two-space-at-end-of-line hard breaks + wouldn't work inside of HTML block elements such as `<p markdown="1">` + where the element expects only span-level content. + +* In the parser code, switched to PHPDoc comment format. Thanks to + Robbie Averill for the help. + + +PHP Markdown Lib 1.6.0 (23 Dec 2015) + +Note: this version was incorrectly released as 1.5.1 on Dec 22, a number +that contradicted the versioning policy. + +* For fenced code blocks in Markdown Extra, can now set a class name for the + code block's language before the special attribute block. Previously, this + class name was only allowed in the absence of the special attribute block. + +* Added a `code_block_content_func` configuration variable which takes a + function that will convert the content of the code block to HTML. This is + most useful for syntax highlighting. For fenced code blocks in Markdown + Extra, the function has access to the language class name (the one outside + of the special attribute block). Credits to Mario Konrad for providing the + implementation. + +* The curled arrow character for the backlink in footnotes is now followed + by a Unicode variant selector to prevent it from being displayed in emoji + form on iOS. + + Note that in older browsers the variant selector is often interpreted as a + separate character, making it visible after the arrow. So there is now a + also a `fn_backlink_html` configuration variable that can be used to set + the link text to something else. Credits to Dana for providing the + implementation. + +* Fixed an issue in MarkdownExtra where long header lines followed by a + special attribute block would hit the backtrack limit an cause an empty + string to be returned. + + +PHP Markdown Lib 1.5.0 (1 Mar 2015) + +* Added the ability start ordered lists with a number different from 1 and + and have that reflected in the HTML output. This can be enabled with + the `enhanced_ordered_lists` configuration variable for the Markdown + parser; it is enabled by default for Markdown Extra. + Credits to Matt Gorle for providing the implementation. + +* Added the ability to insert custom HTML attributes with simple values + everywhere an extra attribute block is allowed (links, images, headers). + The value must be unquoted, cannot contains spaces and is limited to + alphanumeric ASCII characters. + Credits to Peter Droogmans for providing the implementation. + +* Added a `header_id_func` configuration variable which takes a function + that can generate an `id` attribute value from the header text. + Credits to Evert Pot for providing the implementation. + +* Added a `url_filter_func` configuration variable which takes a function + that can rewrite any link or image URL to something different. + + +PHP Markdown Lib 1.4.1 (4 May 2014) + +* The HTML block parser will now treat `<figure>` as a block-level element + (as it should) and no longer wrap it in `<p>` or parse it's content with + the as Markdown syntax (although with Extra you can use `markdown="1"` + if you wish to use the Markdown syntax inside it). + +* The content of `<style>` elements will now be left alone, its content + won't be interpreted as Markdown. + +* Corrected an bug where some inline links with spaces in them would not + work even when surounded with angle brackets: + + [link](<s p a c e s>) + +* Fixed an issue where email addresses with quotes in them would not always + have the quotes escaped in the link attribute, causing broken links (and + invalid HTML). + +* Fixed the case were a link definition following a footnote definition would + be swallowed by the footnote unless it was separated by a blank line. + + +PHP Markdown Lib 1.4.0 (29 Nov 2013) + +* Added support for the `tel:` URL scheme in automatic links. + + <tel:+1-111-111-1111> + + It gets converted to this (note the `tel:` prefix becomes invisible): + + <a href="tel:+1-111-111-1111">+1-111-111-1111</a> + +* Added backtick fenced code blocks to MarkdownExtra, originally from + Github-Flavored Markdown. + +* Added an interface called MarkdownInterface implemented by both + the Markdown and MarkdownExtra parsers. You can use the interface if + you want to create a mockup parser object for unit testing. + +* For those of you who cannot use class autoloading, you can now + include `Michelf/Markdown.inc.php` or `Michelf/MarkdownExtra.inc.php` (note + the `.inc.php` extension) to automatically include other files required + by the parser. + + +PHP Markdown Lib 1.3 (11 Apr 2013) + +This is the first release of PHP Markdown Lib. This package requires PHP +version 5.3 or later and is designed to work with PSR-0 autoloading and, +optionally with Composer. Here is a list of the changes since +PHP Markdown Extra 1.2.6: + +* Plugin interface for WordPress and other systems is no longer present in + the Lib package. The classic package is still available if you need it: + <https://michelf.ca/projects/php-markdown/classic/> + +* Added `public` and `protected` protection attributes, plus a section about + what is "public API" and what isn't in the Readme file. + +* Changed HTML output for footnotes: now instead of adding `rel` and `rev` + attributes, footnotes links have the class name `footnote-ref` and + backlinks `footnote-backref`. + +* Fixed some regular expressions to make PCRE not shout warnings about POSIX + collation classes (dependent on your version of PCRE). + +* Added optional class and id attributes to images and links using the same + syntax as for headers: + + [link](url){#id .class} + ![img](url){#id .class} + + It work too for reference-style links and images. In this case you need + to put those attributes at the reference definition: + + [link][linkref] or [linkref] + ![img][linkref] + + [linkref]: url "optional title" {#id .class} + +* Fixed a PHP notice message triggered when some table column separator + markers are missing on the separator line below column headers. + +* Fixed a small mistake that could cause the parser to retain an invalid + state related to parsing links across multiple runs. This was never + observed (that I know of), but it's still worth fixing. + + +Copyright and License +--------------------- + +PHP Markdown Lib +Copyright (c) 2004-2016 Michel Fortin +<https://michelf.ca/> +All rights reserved. + +Based on Markdown +Copyright (c) 2003-2005 John Gruber +<https://daringfireball.net/> +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. + +* Neither the name "Markdown" nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +This software is provided by the copyright holders and contributors "as +is" and any express or implied warranties, including, but not limited +to, the implied warranties of merchantability and fitness for a +particular purpose are disclaimed. In no event shall the copyright owner +or contributors be liable for any direct, indirect, incidental, special, +exemplary, or consequential damages (including, but not limited to, +procurement of substitute goods or services; loss of use, data, or +profits; or business interruption) however caused and on any theory of +liability, whether in contract, strict liability, or tort (including +negligence or otherwise) arising in any way out of the use of this +software, even if advised of the possibility of such damage. diff --git a/vendor/michelf/php-markdown/Readme.php b/vendor/michelf/php-markdown/Readme.php new file mode 100644 index 000000000..89449dea4 --- /dev/null +++ b/vendor/michelf/php-markdown/Readme.php @@ -0,0 +1,31 @@ +<?php + +// This file passes the content of the Readme.md file in the same directory +// through the Markdown filter. You can adapt this sample code in any way +// you like. + +// Install PSR-0-compatible class autoloader +spl_autoload_register(function($class){ + require preg_replace('{\\\\|_(?!.*\\\\)}', DIRECTORY_SEPARATOR, ltrim($class, '\\')).'.php'; +}); + +// Get Markdown class +use \Michelf\Markdown; + +// Read file and pass content through the Markdown parser +$text = file_get_contents('Readme.md'); +$html = Markdown::defaultTransform($text); + +?> +<!DOCTYPE html> +<html> + <head> + <title>PHP Markdown Lib - Readme</title> + </head> + <body> + <?php + // Put HTML content in the document + echo $html; + ?> + </body> +</html> diff --git a/vendor/michelf/php-markdown/composer.json b/vendor/michelf/php-markdown/composer.json new file mode 100644 index 000000000..36a4f744a --- /dev/null +++ b/vendor/michelf/php-markdown/composer.json @@ -0,0 +1,31 @@ +{ + "name": "michelf/php-markdown", + "type": "library", + "description": "PHP Markdown", + "homepage": "https://michelf.ca/projects/php-markdown/", + "keywords": ["markdown"], + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Michel Fortin", + "email": "michel.fortin@michelf.ca", + "homepage": "https://michelf.ca/", + "role": "Developer" + }, + { + "name": "John Gruber", + "homepage": "https://daringfireball.net/" + } + ], + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-0": { "Michelf": "" } + }, + "extra": { + "branch-alias": { + "dev-lib": "1.4.x-dev" + } + } +} diff --git a/vendor/pixel418/markdownify/CHANGELOG.md b/vendor/pixel418/markdownify/CHANGELOG.md new file mode 100644 index 000000000..a0900fdf5 --- /dev/null +++ b/vendor/pixel418/markdownify/CHANGELOG.md @@ -0,0 +1,76 @@ +CHANGELOG +============== + + +21/09/2016 v2.2.1 +-------------- + + * Fix: Moving trailing whitespace from inline elements outside of the element + * Feature: Use PSR-4 + * Feature: PHP 7.0 support in continuous integration + * Doc: Update of the README + + +07/09/2016 v2.2.0 +-------------- + + * Fix: Reset state between each parsing + + +19/02/2016 v2.1.11 +-------------- + + * Fix: Empty table cell conversion + + +10/02/2016 v2.1.10 +-------------- + + * Fix: Handle nested table. + + +01/04/2015 v2.1.9 +-------------- + + * Fix: Handle HTML breaks & spaces in a less destructive way. + + +26/03/2015 v2.1.8 +-------------- + + * Fix: Use alternative italic character + * Fix: Handle HTML breaks inside another tag + * Fix: Handle HTML spaces around tags + + +07/11/2014 v2.1.7 +-------------- + + * Change composer name to "elephant418/markdownify" + + +14/07/2014 v2.1.6 +-------------- + + * Fix: Simulate a paragraph for inline text preceding block element + * Fix: Nested lists + * Fix: setKeepHTML method + * Feature: PHP 5.5 & 5.6 support in continuous integration + + +16/03/2014 v2.1.5 +-------------- + +Add display settings + + * Test: Add tests for footnotes after every paragraph or not + * Feature: Allow to display link reference in paragraph, without footnotes + + +27/02/2014 v2.1.4 +-------------- + +Improve how ConverterExtra handle id & class attributes: + + * Feature: Allow id & class attributes on links + * Feature: Allow class attributes on headings
\ No newline at end of file diff --git a/vendor/pixel418/markdownify/LICENSE b/vendor/pixel418/markdownify/LICENSE new file mode 100644 index 000000000..5ab7695ab --- /dev/null +++ b/vendor/pixel418/markdownify/LICENSE @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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 + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/vendor/pixel418/markdownify/README.md b/vendor/pixel418/markdownify/README.md new file mode 100644 index 000000000..8855b0d05 --- /dev/null +++ b/vendor/pixel418/markdownify/README.md @@ -0,0 +1,63 @@ +# Markdownify + +[![Build Status](https://travis-ci.org/Elephant418/Markdownify.png?branch=master)](https://travis-ci.org/Elephant418/Markdownify?branch=master) +[![Total Downloads](https://poser.pugx.org/pixel418/markdownify/downloads)](https://packagist.org/packages/pixel418/markdownify) +[![License LGPL](https://poser.pugx.org/pixel418/markdownify/license)](https://opensource.org/licenses/lgpl-2.1.php) + +The HTML to Markdown converter for PHP + +[Code example](#code-example) | [How to Install](#how-to-install) | [How to Contribute](#how-to-contribute) | [Author & Community](#author--community) + + + +Code example +-------- + +### Markdown + +```php +$converter = new Markdownify\Converter; +$converter->parseString('<h1>Heading</h1>'); +// Returns: # Heading +``` + +### Markdown Extra [as defined by @michelf](http://michelf.ca/projects/php-markdown/extra/) + +```php +$converter = new Markdownify\ConverterExtra; +$converter->parseString('<h1 id="md">Heading</h1>'); +// Returns: # Heading {#md} +``` + + + +How to Install +-------- + +This library package requires `PHP 5.3` or later.<br> +Install [Composer](http://getcomposer.org/doc/01-basic-usage.md#installation) and run the following command to get the latest version: + +```sh +composer require pixel418/markdownify +``` + + + +How to Contribute +-------- + +1. Fork the Markdownify repository +2. Create a new branch for each feature or improvement +3. Send a pull request from each feature branch to the **v2.x** branch + +If you don't know much about pull request, you can read [the Github article](https://help.github.com/articles/using-pull-requests) + + + +Author & Community +-------- + +Markdownify is under [LGPL License](http://opensource.org/licenses/LGPL-2.1)<br> +It was created by [Milian Wolff](http://milianw.de)<br> +It was converted to a Symfony Bundle by [Peter Kruithof](https://github.com/pkruithof)<br> +It is maintained by [Thomas ZILLIOX](http://tzi.fr) diff --git a/vendor/pixel418/markdownify/composer.json b/vendor/pixel418/markdownify/composer.json new file mode 100644 index 000000000..71d9f3565 --- /dev/null +++ b/vendor/pixel418/markdownify/composer.json @@ -0,0 +1,38 @@ +{ + "name": "pixel418/markdownify", + "type": "lib", + "description": "The HTML to Markdown converter for PHP ", + "keywords": ["markdown", "markdownify"], + "license": "LGPL", + "homepage": "https://github.com/elephant418/Markdownify", + "authors": [ + { + "name": "Milian Wolff", + "email": "mail@milianw.de", + "homepage": "http://milianw.de" + + }, + { + "name": "Peter Kruithof", + "email": "pkruithof@gmail.com", + "homepage": "http://pkruithof.tumblr.com/" + }, + { + "name": "Thomas Zilliox", + "email": "hello@tzi.fr", + "homepage": "http://tzi.fr" + } + ], + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8" + }, + "autoload": { + "psr-4": { + "Markdownify\\": "src", + "Test\\Markdownify\\": "test" + } + } +} diff --git a/vendor/pixel418/markdownify/src/Converter.php b/vendor/pixel418/markdownify/src/Converter.php new file mode 100644 index 000000000..77c62dc7e --- /dev/null +++ b/vendor/pixel418/markdownify/src/Converter.php @@ -0,0 +1,1400 @@ +<?php + +/* This file is part of the Markdownify project, which is under LGPL license */ + +namespace Markdownify; + +/** + * default configuration + */ +define('MDFY_BODYWIDTH', false); +define('MDFY_KEEPHTML', true); + +/** + * HTML to Markdown converter class + */ +class Converter +{ + /** + * html parser object + * + * @var parseHTML + */ + protected $parser; + + /** + * markdown output + * + * @var string + */ + protected $output; + + /** + * stack with tags which where not converted to html + * + * @var array<string> + */ + protected $notConverted = array(); + + /** + * skip conversion to markdown + * + * @var bool + */ + protected $skipConversion = false; + + /* options */ + + /** + * keep html tags which cannot be converted to markdown + * + * @var bool + */ + protected $keepHTML = false; + + /** + * wrap output, set to 0 to skip wrapping + * + * @var int + */ + protected $bodyWidth = 0; + + /** + * minimum body width + * + * @var int + */ + protected $minBodyWidth = 25; + + /** + * position where the link reference will be displayed + * + * + * @var int + */ + protected $linkPosition; + const LINK_AFTER_CONTENT = 0; + const LINK_AFTER_PARAGRAPH = 1; + const LINK_IN_PARAGRAPH = 2; + + /** + * stores current buffers + * + * @var array<string> + */ + protected $buffer = array(); + + /** + * stores current buffers + * + * @var array<string> + */ + protected $footnotes = array(); + + /** + * tags with elements which can be handled by markdown + * + * @var array<string> + */ + protected $isMarkdownable = array( + 'p' => array(), + 'ul' => array(), + 'ol' => array(), + 'li' => array(), + 'br' => array(), + 'blockquote' => array(), + 'code' => array(), + 'pre' => array(), + 'a' => array( + 'href' => 'required', + 'title' => 'optional', + ), + 'strong' => array(), + 'b' => array(), + 'em' => array(), + 'i' => array(), + 'img' => array( + 'src' => 'required', + 'alt' => 'optional', + 'title' => 'optional', + ), + 'h1' => array(), + 'h2' => array(), + 'h3' => array(), + 'h4' => array(), + 'h5' => array(), + 'h6' => array(), + 'hr' => array(), + ); + + /** + * html tags to be ignored (contents will be parsed) + * + * @var array<string> + */ + protected $ignore = array( + 'html', + 'body', + ); + + /** + * html tags to be dropped (contents will not be parsed!) + * + * @var array<string> + */ + protected $drop = array( + 'script', + 'head', + 'style', + 'form', + 'area', + 'object', + 'param', + 'iframe', + ); + + /** + * html block tags that allow inline & block children + * + * @var array<string> + */ + protected $allowMixedChildren = array( + 'li' + ); + + /** + * Markdown indents which could be wrapped + * @note: use strings in regex format + * + * @var array<string> + */ + protected $wrappableIndents = array( + '\* ', // ul + '\d. ', // ol + '\d\d. ', // ol + '> ', // blockquote + '', // p + ); + + /** + * list of chars which have to be escaped in normal text + * @note: use strings in regex format + * + * @var array + * + * TODO: what's with block chars / sequences at the beginning of a block? + */ + protected $escapeInText = array( + '\*\*([^*]+)\*\*' => '\*\*$1\*\*', // strong + '\*([^*]+)\*' => '\*$1\*', // em + '__(?! |_)(.+)(?!<_| )__' => '\_\_$1\_\_', // strong + '_(?! |_)(.+)(?!<_| )_' => '\_$1\_', // em + '([-*_])([ ]{0,2}\1){2,}' => '\\\\$0', // hr + '`' => '\`', // code + '\[(.+)\](\s*\()' => '\[$1\]$2', // links: [text] (url) => [text\] (url) + '\[(.+)\](\s*)\[(.*)\]' => '\[$1\]$2\[$3\]', // links: [text][id] => [text\][id\] + '^#(#{0,5}) ' => '\#$1 ', // header + ); + + /** + * wether last processed node was a block tag or not + * + * @var bool + */ + protected $lastWasBlockTag = false; + + /** + * name of last closed tag + * + * @var string + */ + protected $lastClosedTag = ''; + + /** + * number of line breaks before next inline output + */ + protected $lineBreaks = 0; + + /** + * node stack, e.g. for <a> and <abbr> tags + * + * @var array<array> + */ + protected $stack = array(); + + /** + * current indentation + * + * @var string + */ + protected $indent = ''; + + /** + * constructor, set options, setup parser + * + * @param int $linkPosition define the position of links + * @param int $bodyWidth whether or not to wrap the output to the given width + * defaults to false + * @param bool $keepHTML whether to keep non markdownable HTML or to discard it + * defaults to true (HTML will be kept) + * @return void + */ + public function __construct($linkPosition = self::LINK_AFTER_CONTENT, $bodyWidth = MDFY_BODYWIDTH, $keepHTML = MDFY_KEEPHTML) + { + $this->linkPosition = $linkPosition; + $this->keepHTML = $keepHTML; + + if ($bodyWidth > $this->minBodyWidth) { + $this->bodyWidth = intval($bodyWidth); + } else { + $this->bodyWidth = false; + } + + $this->parser = new Parser; + $this->parser->noTagsInCode = true; + + // we don't have to do this every time + $search = array(); + $replace = array(); + foreach ($this->escapeInText as $s => $r) { + array_push($search, '@(?<!\\\)' . $s . '@U'); + array_push($replace, $r); + } + $this->escapeInText = array( + 'search' => $search, + 'replace' => $replace + ); + } + + /** + * parse a HTML string + * + * @param string $html + * @return string markdown formatted + */ + public function parseString($html) + { + $this->resetState(); + + $this->parser->html = $html; + $this->parse(); + + return $this->output; + } + + /** + * set the position where the link reference will be displayed + * + * @param int $linkPosition + * @return void + */ + public function setLinkPosition($linkPosition) + { + $this->linkPosition = $linkPosition; + } + + /** + * set keep HTML tags which cannot be converted to markdown + * + * @param bool $linkPosition + * @return void + */ + public function setKeepHTML($keepHTML) + { + $this->keepHTML = $keepHTML; + } + + /** + * iterate through the nodes and decide what we + * shall do with the current node + * + * @param void + * @return void + */ + protected function parse() + { + $this->output = ''; + // drop tags + $this->parser->html = preg_replace('#<(' . implode('|', $this->drop) . ')[^>]*>.*</\\1>#sU', '', $this->parser->html); + while ($this->parser->nextNode()) { + switch ($this->parser->nodeType) { + case 'doctype': + break; + case 'pi': + case 'comment': + if ($this->keepHTML) { + $this->flushLinebreaks(); + $this->out($this->parser->node); + $this->setLineBreaks(2); + } + // else drop + break; + case 'text': + $this->handleText(); + break; + case 'tag': + if (in_array($this->parser->tagName, $this->ignore)) { + break; + } + // If the previous tag was not a block element, we simulate a paragraph tag + if ($this->parser->isBlockElement && $this->parser->isNextToInlineContext && !in_array($this->parent(), $this->allowMixedChildren)) { + $this->setLineBreaks(2); + } + if ($this->parser->isStartTag) { + $this->flushLinebreaks(); + } + if ($this->skipConversion) { + $this->isMarkdownable(); // update notConverted + $this->handleTagToText(); + continue; + } + + // block elements + if (!$this->parser->keepWhitespace && $this->parser->isBlockElement) { + $this->fixBlockElementSpacing(); + } + + // inline elements + if (!$this->parser->keepWhitespace && $this->parser->isInlineContext) { + $this->fixInlineElementSpacing(); + } + + if ($this->isMarkdownable()) { + if ($this->parser->isBlockElement && $this->parser->isStartTag && !$this->lastWasBlockTag && !empty($this->output)) { + if (!empty($this->buffer)) { + $str =& $this->buffer[count($this->buffer) - 1]; + } else { + $str =& $this->output; + } + if (substr($str, -strlen($this->indent) - 1) != "\n" . $this->indent) { + $str .= "\n" . $this->indent; + } + } + $func = 'handleTag_' . $this->parser->tagName; + $this->$func(); + if ($this->linkPosition == self::LINK_AFTER_PARAGRAPH && $this->parser->isBlockElement && !$this->parser->isStartTag && empty($this->parser->openTags)) { + $this->flushFootnotes(); + } + if (!$this->parser->isStartTag) { + $this->lastClosedTag = $this->parser->tagName; + } + } else { + $this->handleTagToText(); + $this->lastClosedTag = ''; + } + break; + default: + trigger_error('invalid node type', E_USER_ERROR); + break; + } + $this->lastWasBlockTag = $this->parser->nodeType == 'tag' && $this->parser->isStartTag && $this->parser->isBlockElement; + } + if (!empty($this->buffer)) { + // trigger_error('buffer was not flushed, this is a bug. please report!', E_USER_WARNING); + while (!empty($this->buffer)) { + $this->out($this->unbuffer()); + } + } + // cleanup + $this->output = rtrim(str_replace('&', '&', str_replace('<', '<', str_replace('>', '>', $this->output)))); + // end parsing, flush stacked tags + $this->flushFootnotes(); + $this->stack = array(); + } + + /** + * check if current tag can be converted to Markdown + * + * @param void + * @return bool + */ + protected function isMarkdownable() + { + if (!isset($this->isMarkdownable[$this->parser->tagName])) { + // simply not markdownable + + return false; + } + if ($this->parser->isStartTag) { + $return = true; + if ($this->keepHTML) { + $diff = array_diff(array_keys($this->parser->tagAttributes), array_keys($this->isMarkdownable[$this->parser->tagName])); + if (!empty($diff)) { + // non markdownable attributes given + $return = false; + } + } + if ($return) { + foreach ($this->isMarkdownable[$this->parser->tagName] as $attr => $type) { + if ($type == 'required' && !isset($this->parser->tagAttributes[$attr])) { + // required markdown attribute not given + $return = false; + break; + } + } + } + if (!$return) { + array_push($this->notConverted, $this->parser->tagName . '::' . implode('/', $this->parser->openTags)); + } + + return $return; + } else { + if (!empty($this->notConverted) && end($this->notConverted) === $this->parser->tagName . '::' . implode('/', $this->parser->openTags)) { + array_pop($this->notConverted); + + return false; + } + + return true; + } + } + + /** + * output footnotes + * + * @param void + * @return void + */ + protected function flushFootnotes() + { + $out = false; + foreach ($this->footnotes as $k => $tag) { + if (!isset($tag['unstacked'])) { + if (!$out) { + $out = true; + $this->out("\n\n", true); + } else { + $this->out("\n", true); + } + $this->out(' [' . $tag['linkID'] . ']: ' . $this->getLinkReference($tag), true); + $tag['unstacked'] = true; + $this->footnotes[$k] = $tag; + } + } + } + + /** + * return formated link reference + * + * @param array $tag + * @return string link reference + */ + protected function getLinkReference($tag) + { + return $tag['href'] . (isset($tag['title']) ? ' "' . $tag['title'] . '"' : ''); + } + + /** + * flush enqued linebreaks + * + * @param void + * @return void + */ + protected function flushLinebreaks() + { + if ($this->lineBreaks && !empty($this->output)) { + $this->out(str_repeat("\n" . $this->indent, $this->lineBreaks), true); + } + $this->lineBreaks = 0; + } + + /** + * handle non Markdownable tags + * + * @param void + * @return void + */ + protected function handleTagToText() + { + if (!$this->keepHTML) { + if (!$this->parser->isStartTag && $this->parser->isBlockElement) { + $this->setLineBreaks(2); + } + } else { + // dont convert to markdown inside this tag + /** TODO: markdown extra **/ + if (!$this->parser->isEmptyTag) { + if ($this->parser->isStartTag) { + if (!$this->skipConversion) { + $this->skipConversion = $this->parser->tagName . '::' . implode('/', $this->parser->openTags); + } + } else { + if ($this->skipConversion == $this->parser->tagName . '::' . implode('/', $this->parser->openTags)) { + $this->skipConversion = false; + } + } + } + + if ($this->parser->isBlockElement) { + if ($this->parser->isStartTag) { + // looks like ins or del are block elements now + if (in_array($this->parent(), array('ins', 'del'))) { + $this->out("\n", true); + $this->indent(' '); + } + // don't indent inside <pre> tags + if ($this->parser->tagName == 'pre') { + $this->out($this->parser->node); + static $indent; + $indent = $this->indent; + $this->indent = ''; + } else { + $this->out($this->parser->node . "\n" . $this->indent); + if (!$this->parser->isEmptyTag) { + $this->indent(' '); + } else { + $this->setLineBreaks(1); + } + $this->parser->html = ltrim($this->parser->html); + } + } else { + if (!$this->parser->keepWhitespace) { + $this->output = rtrim($this->output); + } + if ($this->parser->tagName != 'pre') { + $this->indent(' '); + $this->out("\n" . $this->indent . $this->parser->node); + } else { + // reset indentation + $this->out($this->parser->node); + static $indent; + $this->indent = $indent; + } + + if (in_array($this->parent(), array('ins', 'del'))) { + // ins or del was block element + $this->out("\n"); + $this->indent(' '); + } + if ($this->parser->tagName == 'li') { + $this->setLineBreaks(1); + } else { + $this->setLineBreaks(2); + } + } + } else { + $this->out($this->parser->node); + } + if (in_array($this->parser->tagName, array('code', 'pre'))) { + if ($this->parser->isStartTag) { + $this->buffer(); + } else { + // add stuff so cleanup just reverses this + $this->out(str_replace('<', '&lt;', str_replace('>', '&gt;', $this->unbuffer()))); + } + } + } + } + + /** + * handle plain text + * + * @param void + * @return void + */ + protected function handleText() + { + if ($this->hasParent('pre') && strpos($this->parser->node, "\n") !== false) { + $this->parser->node = str_replace("\n", "\n" . $this->indent, $this->parser->node); + } + if (!$this->hasParent('code') && !$this->hasParent('pre')) { + // entity decode + $this->parser->node = $this->decode($this->parser->node); + if (!$this->skipConversion) { + // escape some chars in normal Text + $this->parser->node = preg_replace($this->escapeInText['search'], $this->escapeInText['replace'], $this->parser->node); + } + } else { + $this->parser->node = str_replace(array('"', '&apos'), array('"', '\''), $this->parser->node); + } + $this->out($this->parser->node); + $this->lastClosedTag = ''; + } + + /** + * handle <em> and <i> tags + * + * @param void + * @return void + */ + protected function handleTag_em() + { + $this->out('_', true); + } + + protected function handleTag_i() + { + $this->handleTag_em(); + } + + /** + * handle <strong> and <b> tags + * + * @param void + * @return void + */ + protected function handleTag_strong() + { + $this->out('**', true); + } + + protected function handleTag_b() + { + $this->handleTag_strong(); + } + + /** + * handle <h1> tags + * + * @param void + * @return void + */ + protected function handleTag_h1() + { + $this->handleHeader(1); + } + + /** + * handle <h2> tags + * + * @param void + * @return void + */ + protected function handleTag_h2() + { + $this->handleHeader(2); + } + + /** + * handle <h3> tags + * + * @param void + * @return void + */ + protected function handleTag_h3() + { + $this->handleHeader(3); + } + + /** + * handle <h4> tags + * + * @param void + * @return void + */ + protected function handleTag_h4() + { + $this->handleHeader(4); + } + + /** + * handle <h5> tags + * + * @param void + * @return void + */ + protected function handleTag_h5() + { + $this->handleHeader(5); + } + + /** + * handle <h6> tags + * + * @param void + * @return void + */ + protected function handleTag_h6() + { + $this->handleHeader(6); + } + + /** + * handle header tags (<h1> - <h6>) + * + * @param int $level 1-6 + * @return void + */ + protected function handleHeader($level) + { + if ($this->parser->isStartTag) { + $this->out(str_repeat('#', $level) . ' ', true); + } else { + $this->setLineBreaks(2); + } + } + + /** + * handle <p> tags + * + * @param void + * @return void + */ + protected function handleTag_p() + { + if (!$this->parser->isStartTag) { + $this->setLineBreaks(2); + } + } + + /** + * handle <a> tags + * + * @param void + * @return void + */ + protected function handleTag_a() + { + if ($this->parser->isStartTag) { + $this->buffer(); + $this->handleTag_a_parser(); + $this->stack(); + } else { + $tag = $this->unstack(); + $buffer = $this->unbuffer(); + $this->handleTag_a_converter($tag, $buffer); + $this->out($this->handleTag_a_converter($tag, $buffer), true); + } + } + + /** + * handle <a> tags parsing + * + * @param void + * @return void + */ + protected function handleTag_a_parser() + { + if (isset($this->parser->tagAttributes['title'])) { + $this->parser->tagAttributes['title'] = $this->decode($this->parser->tagAttributes['title']); + } else { + $this->parser->tagAttributes['title'] = null; + } + $this->parser->tagAttributes['href'] = $this->decode(trim($this->parser->tagAttributes['href'])); + } + + /** + * handle <a> tags conversion + * + * @param array $tag + * @param string $buffer + * @return string The markdownified link + */ + protected function handleTag_a_converter($tag, $buffer) + { + if (empty($tag['href']) && empty($tag['title'])) { + // empty links... testcase mania, who would possibly do anything like that?! + return '[' . $buffer . ']()'; + } + + if ($buffer == $tag['href'] && empty($tag['title'])) { + // <http://example.com> + return '<' . $buffer . '>'; + } + + $bufferDecoded = $this->decode(trim($buffer)); + if (substr($tag['href'], 0, 7) == 'mailto:' && 'mailto:' . $bufferDecoded == $tag['href']) { + if (is_null($tag['title'])) { + // <mail@example.com> + return '<' . $bufferDecoded . '>'; + } + // [mail@example.com][1] + // ... + // [1]: mailto:mail@example.com Title + $tag['href'] = 'mailto:' . $bufferDecoded; + } + + if ($this->linkPosition == self::LINK_IN_PARAGRAPH) { + return '[' . $buffer . '](' . $this->getLinkReference($tag) . ')'; + } + + // [This link][id] + foreach ($this->footnotes as $tag2) { + if ($tag2['href'] == $tag['href'] && $tag2['title'] === $tag['title']) { + $tag['linkID'] = $tag2['linkID']; + break; + } + } + if (!isset($tag['linkID'])) { + $tag['linkID'] = count($this->footnotes) + 1; + array_push($this->footnotes, $tag); + } + + return '[' . $buffer . '][' . $tag['linkID'] . ']'; + } + + /** + * handle <img /> tags + * + * @param void + * @return void + */ + protected function handleTag_img() + { + if (!$this->parser->isStartTag) { + return; // just to be sure this is really an empty tag... + } + + if (isset($this->parser->tagAttributes['title'])) { + $this->parser->tagAttributes['title'] = $this->decode($this->parser->tagAttributes['title']); + } else { + $this->parser->tagAttributes['title'] = null; + } + if (isset($this->parser->tagAttributes['alt'])) { + $this->parser->tagAttributes['alt'] = $this->decode($this->parser->tagAttributes['alt']); + } else { + $this->parser->tagAttributes['alt'] = null; + } + + if (empty($this->parser->tagAttributes['src'])) { + // support for "empty" images... dunno if this is really needed + // but there are some test cases which do that... + if (!empty($this->parser->tagAttributes['title'])) { + $this->parser->tagAttributes['title'] = ' ' . $this->parser->tagAttributes['title'] . ' '; + } + $this->out('![' . $this->parser->tagAttributes['alt'] . '](' . $this->parser->tagAttributes['title'] . ')', true); + + return; + } else { + $this->parser->tagAttributes['src'] = $this->decode($this->parser->tagAttributes['src']); + } + + $out = '![' . $this->parser->tagAttributes['alt'] . ']'; + if ($this->linkPosition == self::LINK_IN_PARAGRAPH) { + $out .= '(' . $this->parser->tagAttributes['src']; + if ($this->parser->tagAttributes['title']) { + $out .= ' "' . $this->parser->tagAttributes['title'] . '"'; + } + $out .= ')'; + $this->out($out, true); + return; + } + + // ![This image][id] + $link_id = false; + if (!empty($this->footnotes)) { + foreach ($this->footnotes as $tag) { + if ($tag['href'] == $this->parser->tagAttributes['src'] + && $tag['title'] === $this->parser->tagAttributes['title'] + ) { + $link_id = $tag['linkID']; + break; + } + } + } + if (!$link_id) { + $link_id = count($this->footnotes) + 1; + $tag = array( + 'href' => $this->parser->tagAttributes['src'], + 'linkID' => $link_id, + 'title' => $this->parser->tagAttributes['title'] + ); + array_push($this->footnotes, $tag); + } + $out .= '[' . $link_id . ']'; + + $this->out($out, true); + } + + /** + * handle <code> tags + * + * @param void + * @return void + */ + protected function handleTag_code() + { + if ($this->hasParent('pre')) { + // ignore code blocks inside <pre> + + return; + } + if ($this->parser->isStartTag) { + $this->buffer(); + } else { + $buffer = $this->unbuffer(); + // use as many backticks as needed + preg_match_all('#`+#', $buffer, $matches); + if (!empty($matches[0])) { + rsort($matches[0]); + + $ticks = '`'; + while (true) { + if (!in_array($ticks, $matches[0])) { + break; + } + $ticks .= '`'; + } + } else { + $ticks = '`'; + } + if ($buffer[0] == '`' || substr($buffer, -1) == '`') { + $buffer = ' ' . $buffer . ' '; + } + $this->out($ticks . $buffer . $ticks, true); + } + } + + /** + * handle <pre> tags + * + * @param void + * @return void + */ + protected function handleTag_pre() + { + if ($this->keepHTML && $this->parser->isStartTag) { + // check if a simple <code> follows + if (!preg_match('#^\s*<code\s*>#Us', $this->parser->html)) { + // this is no standard markdown code block + $this->handleTagToText(); + + return; + } + } + $this->indent(' '); + if (!$this->parser->isStartTag) { + $this->setLineBreaks(2); + } else { + $this->parser->html = ltrim($this->parser->html); + } + } + + /** + * handle <blockquote> tags + * + * @param void + * @return void + */ + protected function handleTag_blockquote() + { + $this->indent('> '); + } + + /** + * handle <ul> tags + * + * @param void + * @return void + */ + protected function handleTag_ul() + { + if ($this->parser->isStartTag) { + $this->stack(); + if (!$this->keepHTML && $this->lastClosedTag == $this->parser->tagName) { + $this->out("\n" . $this->indent . '<!-- -->' . "\n" . $this->indent . "\n" . $this->indent); + } + } else { + $this->unstack(); + if ($this->parent() != 'li' || preg_match('#^\s*(</li\s*>\s*<li\s*>\s*)?<(p|blockquote)\s*>#sU', $this->parser->html)) { + // dont make Markdown add unneeded paragraphs + $this->setLineBreaks(2); + } + } + } + + /** + * handle <ul> tags + * + * @param void + * @return void + */ + protected function handleTag_ol() + { + // same as above + $this->parser->tagAttributes['num'] = 0; + $this->handleTag_ul(); + } + + /** + * handle <li> tags + * + * @param void + * @return void + */ + protected function handleTag_li() + { + if ($this->parent() == 'ol') { + $parent =& $this->getStacked('ol'); + if ($this->parser->isStartTag) { + $parent['num']++; + $this->out(str_repeat(' ', 3 - strlen($parent['num'])) . $parent['num'] . '. ', true); + } + } else { + if ($this->parser->isStartTag) { + $this->out(' * ', true); + } + } + $this->indent(' ', false); + if (!$this->parser->isStartTag) { + $this->setLineBreaks(1); + } + } + + /** + * handle <hr /> tags + * + * @param void + * @return void + */ + protected function handleTag_hr() + { + if (!$this->parser->isStartTag) { + return; // just to be sure this really is an empty tag + } + $this->out('* * *', true); + $this->setLineBreaks(2); + } + + /** + * handle <br /> tags + * + * @param void + * @return void + */ + protected function handleTag_br() + { + $this->out(" \n" . $this->indent, true); + $this->parser->html = ltrim($this->parser->html); + } + + /** + * add current node to the stack + * this only stores the attributes + * + * @param void + * @return void + */ + protected function stack() + { + if (!isset($this->stack[$this->parser->tagName])) { + $this->stack[$this->parser->tagName] = array(); + } + array_push($this->stack[$this->parser->tagName], $this->parser->tagAttributes); + } + + /** + * remove current tag from stack + * + * @param void + * @return array + */ + protected function unstack() + { + if (!isset($this->stack[$this->parser->tagName]) || !is_array($this->stack[$this->parser->tagName])) { + trigger_error('Trying to unstack from empty stack. This must not happen.', E_USER_ERROR); + } + + return array_pop($this->stack[$this->parser->tagName]); + } + + /** + * get last stacked element of type $tagName + * + * @param string $tagName + * @return array + */ + protected function &getStacked($tagName) + { + // no end() so it can be referenced + return $this->stack[$tagName][count($this->stack[$tagName]) - 1]; + } + + /** + * set number of line breaks before next start tag + * + * @param int $number + * @return void + */ + protected function setLineBreaks($number) + { + if ($this->lineBreaks < $number) { + $this->lineBreaks = $number; + } + } + + /** + * buffer next parser output until unbuffer() is called + * + * @param void + * @return void + */ + protected function buffer() + { + array_push($this->buffer, ''); + } + + /** + * end current buffer and return buffered output + * + * @param void + * @return string + */ + protected function unbuffer() + { + return array_pop($this->buffer); + } + + /** + * append string to the correct var, either + * directly to $this->output or to the current + * buffers + * + * @param string $put + * @param boolean $nowrap + * @return void + */ + protected function out($put, $nowrap = false) + { + if (empty($put)) { + return; + } + if (!empty($this->buffer)) { + $this->buffer[count($this->buffer) - 1] .= $put; + } else { + if ($this->bodyWidth && !$this->parser->keepWhitespace) { // wrap lines + // get last line + $pos = strrpos($this->output, "\n"); + if ($pos === false) { + $line = $this->output; + } else { + $line = substr($this->output, $pos); + } + + if ($nowrap) { + if ($put[0] != "\n" && $this->strlen($line) + $this->strlen($put) > $this->bodyWidth) { + $this->output .= "\n" . $this->indent . $put; + } else { + $this->output .= $put; + } + + return; + } else { + $put .= "\n"; // make sure we get all lines in the while below + $lineLen = $this->strlen($line); + while ($pos = strpos($put, "\n")) { + $putLine = substr($put, 0, $pos + 1); + $put = substr($put, $pos + 1); + $putLen = $this->strlen($putLine); + if ($lineLen + $putLen < $this->bodyWidth) { + $this->output .= $putLine; + $lineLen = $putLen; + } else { + $split = preg_split('#^(.{0,' . ($this->bodyWidth - $lineLen) . '})\b#', $putLine, 2, PREG_SPLIT_OFFSET_CAPTURE | PREG_SPLIT_DELIM_CAPTURE); + $this->output .= rtrim($split[1][0]) . "\n" . $this->indent . $this->wordwrap(ltrim($split[2][0]), $this->bodyWidth, "\n" . $this->indent, false); + } + } + $this->output = substr($this->output, 0, -1); + + return; + } + } else { + $this->output .= $put; + } + } + } + + /** + * indent next output (start tag) or unindent (end tag) + * + * @param string $str indentation + * @param bool $output add indendation to output + * @return void + */ + protected function indent($str, $output = true) + { + if ($this->parser->isStartTag) { + $this->indent .= $str; + if ($output) { + $this->out($str, true); + } + } else { + $this->indent = substr($this->indent, 0, -strlen($str)); + } + } + + /** + * decode email addresses + * + * @author derernst@gmx.ch <http://www.php.net/manual/en/function.html-entity-decode.php#68536> + * @author Milian Wolff <http://milianw.de> + */ + protected function decode($text, $quote_style = ENT_QUOTES) + { + return htmlspecialchars_decode($text, $quote_style); + } + + /** + * callback for decode() which converts a hexadecimal entity to UTF-8 + * + * @param array $matches + * @return string UTF-8 encoded + */ + protected function _decode_hex($matches) + { + return $this->unichr(hexdec($matches[1])); + } + + /** + * callback for decode() which converts a numerical entity to UTF-8 + * + * @param array $matches + * @return string UTF-8 encoded + */ + protected function _decode_numeric($matches) + { + return $this->unichr($matches[1]); + } + + /** + * UTF-8 chr() which supports numeric entities + * + * @author grey - greywyvern - com <http://www.php.net/manual/en/function.chr.php#55978> + * @param array $matches + * @return string UTF-8 encoded + */ + protected function unichr($dec) + { + if ($dec < 128) { + $utf = chr($dec); + } elseif ($dec < 2048) { + $utf = chr(192 + (($dec - ($dec % 64)) / 64)); + $utf .= chr(128 + ($dec % 64)); + } else { + $utf = chr(224 + (($dec - ($dec % 4096)) / 4096)); + $utf .= chr(128 + ((($dec % 4096) - ($dec % 64)) / 64)); + $utf .= chr(128 + ($dec % 64)); + } + + return $utf; + } + + /** + * UTF-8 strlen() + * + * @param string $str + * @return int + * + * @author dtorop 932 at hotmail dot com <http://www.php.net/manual/en/function.strlen.php#37975> + * @author Milian Wolff <http://milianw.de> + */ + protected function strlen($str) + { + if (function_exists('mb_strlen')) { + return mb_strlen($str, 'UTF-8'); + } else { + return preg_match_all('/[\x00-\x7F\xC0-\xFD]/', $str, $var_empty); + } + } + + /** + * wordwrap for utf8 encoded strings + * + * @param string $str + * @param integer $len + * @param string $what + * @return string + */ + protected function wordwrap($str, $width, $break, $cut = false) + { + if (!$cut) { + $regexp = '#^(?:[\x00-\x7F]|[\xC0-\xFF][\x80-\xBF]+){1,' . $width . '}\b#'; + } else { + $regexp = '#^(?:[\x00-\x7F]|[\xC0-\xFF][\x80-\xBF]+){' . $width . '}#'; + } + $return = ''; + while (preg_match($regexp, $str, $matches)) { + $string = $matches[0]; + $str = ltrim(substr($str, strlen($string))); + if (!$cut && isset($str[0]) && in_array($str[0], array('.', '!', ';', ':', '?', ','))) { + $string .= $str[0]; + $str = ltrim(substr($str, 1)); + } + $return .= $string . $break; + } + + return $return . ltrim($str); + } + + /** + * check if current node has a $tagName as parent (somewhere, not only the direct parent) + * + * @param string $tagName + * @return bool + */ + protected function hasParent($tagName) + { + return in_array($tagName, $this->parser->openTags); + } + + /** + * get tagName of direct parent tag + * + * @param void + * @return string $tagName + */ + protected function parent() + { + return end($this->parser->openTags); + } + + /** + * Trims whitespace in block-level elements, on the left side. + */ + protected function fixBlockElementSpacing() + { + if ($this->parser->isStartTag) { + $this->parser->html = ltrim($this->parser->html); + } + } + + /** + * Moves leading/trailing whitespace from inline elements outside of the + * element. This is to fix cases like `<strong> Text</strong>`, which if + * converted to `** strong**` would be incorrect Markdown. + * + * Examples: + * + * * leading: `<strong> Text</strong>` becomes ` <strong>Text</strong>` + * * trailing: `<strong>Text </strong>` becomes `<strong>Text</strong> ` + */ + protected function fixInlineElementSpacing() + { + if ($this->parser->isStartTag) { + // move spaces after the start element to before the element + if (preg_match('~^(\s+)~', $this->parser->html, $matches)) { + $this->out($matches[1]); + $this->parser->html = ltrim($this->parser->html, " \t\0\x0B"); + } + } else { + if (!empty($this->buffer)) { + $str =& $this->buffer[count($this->buffer) - 1]; + } else { + $str =& $this->output; + } + + // move spaces before the end element to after the element + if (preg_match('~(\s+)$~', $str, $matches)) { + $str = rtrim($str, " \t\0\x0B"); + $this->parser->html = $matches[1] . $this->parser->html; + } + } + } + + /** + * Resetting the state forces the instance to behave as a fresh instance. + * Ideal for running within a loop where you want to maintain a single instance. + */ + protected function resetState() + { + $this->notConverted = array(); + $this->skipConversion = false; + $this->buffer = array(); + $this->indent = ''; + $this->stack = array(); + $this->lineBreaks = 0; + $this->lastClosedTag = ''; + $this->lastWasBlockTag = false; + $this->footnotes = array(); + } +} diff --git a/vendor/pixel418/markdownify/src/ConverterExtra.php b/vendor/pixel418/markdownify/src/ConverterExtra.php new file mode 100644 index 000000000..733955448 --- /dev/null +++ b/vendor/pixel418/markdownify/src/ConverterExtra.php @@ -0,0 +1,573 @@ +<?php + +/* This file is part of the Markdownify project, which is under LGPL license */ + +namespace Markdownify; + +class ConverterExtra extends Converter +{ + + /** + * table data, including rows with content and the maximum width of each col + * + * @var array + */ + protected $table = array(); + + /** + * current col + * + * @var int + */ + protected $col = -1; + + /** + * current row + * + * @var int + */ + protected $row = 0; + + /** + * constructor, see Markdownify::Markdownify() for more information + */ + public function __construct($linksAfterEachParagraph = self::LINK_AFTER_CONTENT, $bodyWidth = MDFY_BODYWIDTH, $keepHTML = MDFY_KEEPHTML) + { + parent::__construct($linksAfterEachParagraph, $bodyWidth, $keepHTML); + + // new markdownable tags & attributes + // header ids: # foo {bar} + $this->isMarkdownable['h1']['id'] = 'optional'; + $this->isMarkdownable['h1']['class'] = 'optional'; + $this->isMarkdownable['h2']['id'] = 'optional'; + $this->isMarkdownable['h2']['class'] = 'optional'; + $this->isMarkdownable['h3']['id'] = 'optional'; + $this->isMarkdownable['h3']['class'] = 'optional'; + $this->isMarkdownable['h4']['id'] = 'optional'; + $this->isMarkdownable['h4']['class'] = 'optional'; + $this->isMarkdownable['h5']['id'] = 'optional'; + $this->isMarkdownable['h5']['class'] = 'optional'; + $this->isMarkdownable['h6']['id'] = 'optional'; + $this->isMarkdownable['h6']['class'] = 'optional'; + // tables + $this->isMarkdownable['table'] = array(); + $this->isMarkdownable['th'] = array( + 'align' => 'optional', + ); + $this->isMarkdownable['td'] = array( + 'align' => 'optional', + ); + $this->isMarkdownable['tr'] = array(); + array_push($this->ignore, 'thead'); + array_push($this->ignore, 'tbody'); + array_push($this->ignore, 'tfoot'); + // definition lists + $this->isMarkdownable['dl'] = array(); + $this->isMarkdownable['dd'] = array(); + $this->isMarkdownable['dt'] = array(); + // link class + $this->isMarkdownable['a']['id'] = 'optional'; + $this->isMarkdownable['a']['class'] = 'optional'; + // footnotes + $this->isMarkdownable['fnref'] = array( + 'target' => 'required', + ); + $this->isMarkdownable['footnotes'] = array(); + $this->isMarkdownable['fn'] = array( + 'name' => 'required', + ); + $this->parser->blockElements['fnref'] = false; + $this->parser->blockElements['fn'] = true; + $this->parser->blockElements['footnotes'] = true; + // abbr + $this->isMarkdownable['abbr'] = array( + 'title' => 'required', + ); + // build RegEx lookahead to decide wether table can pe parsed or not + $inlineTags = array_keys($this->parser->blockElements, false); + $colContents = '(?:[^<]|<(?:' . implode('|', $inlineTags) . '|[^a-z]))*'; + $this->tableLookaheadHeader = '{ + ^\s*(?:<thead\s*>)?\s* # open optional thead + <tr\s*>\s*(?: # start required row with headers + <th(?:\s+align=("|\')(?:left|center|right)\1)?\s*> # header with optional align + \s*' . $colContents . '\s* # contents + </th>\s* # close header + )+</tr> # close row with headers + \s*(?:</thead>)? # close optional thead + }sxi'; + $this->tdSubstitute = '\s*' . $colContents . '\s* # contents + </td>\s*'; + $this->tableLookaheadBody = '{ + \s*(?:<tbody\s*>)?\s* # open optional tbody + (?:<tr\s*>\s* # start row + %s # cols to be substituted + </tr>)+ # close row + \s*(?:</tbody>)? # close optional tbody + \s*</table> # close table + }sxi'; + } + + /** + * handle header tags (<h1> - <h6>) + * + * @param int $level 1-6 + * @return void + */ + protected function handleHeader($level) + { + if ($this->parser->isStartTag) { + $this->parser->tagAttributes['cssSelector'] = $this->getCurrentCssSelector(); + $this->stack(); + } else { + $tag = $this->unstack(); + if (!empty($tag['cssSelector'])) { + // {#id.class} + $this->out(' {' . $tag['cssSelector'] . '}'); + } + } + parent::handleHeader($level); + } + + /** + * handle <a> tags parsing + * + * @param void + * @return void + */ + protected function handleTag_a_parser() + { + parent::handleTag_a_parser(); + $this->parser->tagAttributes['cssSelector'] = $this->getCurrentCssSelector(); + } + + /** + * handle <a> tags conversion + * + * @param array $tag + * @param string $buffer + * @return string The markdownified link + */ + protected function handleTag_a_converter($tag, $buffer) + { + $output = parent::handleTag_a_converter($tag, $buffer); + if (!empty($tag['cssSelector'])) { + // [This link][id]{#id.class} + $output .= '{' . $tag['cssSelector'] . '}'; + } + + return $output; + } + + /** + * handle <abbr> tags + * + * @param void + * @return void + */ + protected function handleTag_abbr() + { + if ($this->parser->isStartTag) { + $this->stack(); + $this->buffer(); + } else { + $tag = $this->unstack(); + $tag['text'] = $this->unbuffer(); + $add = true; + foreach ($this->stack['abbr'] as $stacked) { + if ($stacked['text'] == $tag['text']) { + /** TODO: differing abbr definitions, i.e. different titles for same text **/ + $add = false; + break; + } + } + $this->out($tag['text']); + if ($add) { + array_push($this->stack['abbr'], $tag); + } + } + } + + /** + * flush stacked abbr tags + * + * @param void + * @return void + */ + protected function flushStacked_abbr() + { + $out = array(); + foreach ($this->stack['abbr'] as $k => $tag) { + if (!isset($tag['unstacked'])) { + array_push($out, ' *[' . $tag['text'] . ']: ' . $tag['title']); + $tag['unstacked'] = true; + $this->stack['abbr'][$k] = $tag; + } + } + if (!empty($out)) { + $this->out("\n\n" . implode("\n", $out)); + } + } + + /** + * handle <table> tags + * + * @param void + * @return void + */ + protected function handleTag_table() + { + if ($this->parser->isStartTag) { + // check if upcoming table can be converted + if ($this->keepHTML) { + if (preg_match($this->tableLookaheadHeader, $this->parser->html, $matches)) { + // header seems good, now check body + // get align & number of cols + preg_match_all('#<th(?:\s+align=("|\')(left|right|center)\1)?\s*>#si', $matches[0], $cols); + $regEx = ''; + $i = 1; + $aligns = array(); + foreach ($cols[2] as $align) { + $align = strtolower($align); + array_push($aligns, $align); + if (empty($align)) { + $align = 'left'; // default value + } + $td = '\s+align=("|\')' . $align . '\\' . $i; + $i++; + if ($align == 'left') { + // look for empty align or left + $td = '(?:' . $td . ')?'; + } + $td = '<td' . $td . '\s*>'; + $regEx .= $td . $this->tdSubstitute; + } + $regEx = sprintf($this->tableLookaheadBody, $regEx); + if (preg_match($regEx, $this->parser->html, $matches, null, strlen($matches[0]))) { + // this is a markdownable table tag! + $this->table = array( + 'rows' => array(), + 'col_widths' => array(), + 'aligns' => $aligns, + ); + $this->row = 0; + } else { + // non markdownable table + $this->handleTagToText(); + } + } else { + // non markdownable table + $this->handleTagToText(); + } + } else { + $this->table = array( + 'rows' => array(), + 'col_widths' => array(), + 'aligns' => array(), + ); + $this->row = 0; + } + } else { + // finally build the table in Markdown Extra syntax + $separator = array(); + if (!isset($this->table['aligns'])) { + $this->table['aligns'] = array(); + } + // seperator with correct align identifiers + foreach ($this->table['aligns'] as $col => $align) { + if (!$this->keepHTML && !isset($this->table['col_widths'][$col])) { + break; + } + $left = ' '; + $right = ' '; + switch ($align) { + case 'left': + $left = ':'; + break; + case 'center': + $right = ':'; + $left = ':'; + case 'right': + $right = ':'; + break; + } + array_push($separator, $left . str_repeat('-', $this->table['col_widths'][$col]) . $right); + } + $separator = '|' . implode('|', $separator) . '|'; + + $rows = array(); + // add padding + array_walk_recursive($this->table['rows'], array(&$this, 'alignTdContent')); + $header = array_shift($this->table['rows']); + array_push($rows, '| ' . implode(' | ', $header) . ' |'); + array_push($rows, $separator); + foreach ($this->table['rows'] as $row) { + array_push($rows, '| ' . implode(' | ', $row) . ' |'); + } + $this->out(implode("\n" . $this->indent, $rows)); + $this->table = array(); + $this->setLineBreaks(2); + } + } + + /** + * properly pad content so it is aligned as whished + * should be used with array_walk_recursive on $this->table['rows'] + * + * @param string &$content + * @param int $col + * @return void + */ + protected function alignTdContent(&$content, $col) + { + if (!isset($this->table['aligns'][$col])) { + $this->table['aligns'][$col] = 'left'; + } + switch ($this->table['aligns'][$col]) { + default: + case 'left': + $content .= str_repeat(' ', $this->table['col_widths'][$col] - $this->strlen($content)); + break; + case 'right': + $content = str_repeat(' ', $this->table['col_widths'][$col] - $this->strlen($content)) . $content; + break; + case 'center': + $paddingNeeded = $this->table['col_widths'][$col] - $this->strlen($content); + $left = floor($paddingNeeded / 2); + $right = $paddingNeeded - $left; + $content = str_repeat(' ', $left) . $content . str_repeat(' ', $right); + break; + } + } + + /** + * handle <tr> tags + * + * @param void + * @return void + */ + protected function handleTag_tr() + { + if ($this->parser->isStartTag) { + $this->col = -1; + } else { + $this->row++; + } + } + + /** + * handle <td> tags + * + * @param void + * @return void + */ + protected function handleTag_td() + { + if ($this->parser->isStartTag) { + $this->col++; + if (!isset($this->table['col_widths'][$this->col])) { + $this->table['col_widths'][$this->col] = 0; + } + $this->buffer(); + } else { + $buffer = trim($this->unbuffer()); + if (!isset($this->table['col_widths'][$this->col])) { + $this->table['col_widths'][$this->col] = 0; + } + $this->table['col_widths'][$this->col] = max($this->table['col_widths'][$this->col], $this->strlen($buffer)); + $this->table['rows'][$this->row][$this->col] = $buffer; + } + } + + /** + * handle <th> tags + * + * @param void + * @return void + */ + protected function handleTag_th() + { + if (!$this->keepHTML && !isset($this->table['rows'][1]) && !isset($this->table['aligns'][$this->col + 1])) { + if (isset($this->parser->tagAttributes['align'])) { + $this->table['aligns'][$this->col + 1] = $this->parser->tagAttributes['align']; + } else { + $this->table['aligns'][$this->col + 1] = ''; + } + } + $this->handleTag_td(); + } + + /** + * handle <dl> tags + * + * @param void + * @return void + */ + protected function handleTag_dl() + { + if (!$this->parser->isStartTag) { + $this->setLineBreaks(2); + } + } + + /** + * handle <dt> tags + * + * @param void + * @return void + **/ + protected function handleTag_dt() + { + if (!$this->parser->isStartTag) { + $this->setLineBreaks(1); + } + } + + /** + * handle <dd> tags + * + * @param void + * @return void + */ + protected function handleTag_dd() + { + if ($this->parser->isStartTag) { + if (substr(ltrim($this->parser->html), 0, 3) == '<p>') { + // next comes a paragraph, so we'll need an extra line + $this->out("\n" . $this->indent); + } elseif (substr($this->output, -2) == "\n\n") { + $this->output = substr($this->output, 0, -1); + } + $this->out(': '); + $this->indent(' ', false); + } else { + // lookahead for next dt + if (substr(ltrim($this->parser->html), 0, 4) == '<dt>') { + $this->setLineBreaks(2); + } else { + $this->setLineBreaks(1); + } + $this->indent(' '); + } + } + + /** + * handle <fnref /> tags (custom footnote references, see markdownify_extra::parseString()) + * + * @param void + * @return void + */ + protected function handleTag_fnref() + { + $this->out('[^' . $this->parser->tagAttributes['target'] . ']'); + } + + /** + * handle <fn> tags (custom footnotes, see markdownify_extra::parseString() + * and markdownify_extra::_makeFootnotes()) + * + * @param void + * @return void + */ + protected function handleTag_fn() + { + if ($this->parser->isStartTag) { + $this->out('[^' . $this->parser->tagAttributes['name'] . ']:'); + $this->setLineBreaks(1); + } else { + $this->setLineBreaks(2); + } + $this->indent(' '); + } + + /** + * handle <footnotes> tag (custom footnotes, see markdownify_extra::parseString() + * and markdownify_extra::_makeFootnotes()) + * + * @param void + * @return void + */ + protected function handleTag_footnotes() + { + if (!$this->parser->isStartTag) { + $this->setLineBreaks(2); + } + } + + /** + * parse a HTML string, clean up footnotes prior + * + * @param string $HTML input + * @return string Markdown formatted output + */ + public function parseString($html) + { + /** TODO: custom markdown-extra options, e.g. titles & classes **/ + // <sup id="fnref:..."><a href"#fn..." rel="footnote">...</a></sup> + // => <fnref target="..." /> + $html = preg_replace('@<sup id="fnref:([^"]+)">\s*<a href="#fn:\1" rel="footnote">\s*\d+\s*</a>\s*</sup>@Us', '<fnref target="$1" />', $html); + // <div class="footnotes"> + // <hr /> + // <ol> + // + // <li id="fn:...">...</li> + // ... + // + // </ol> + // </div> + // => + // <footnotes> + // <fn name="...">...</fn> + // ... + // </footnotes> + $html = preg_replace_callback('#<div class="footnotes">\s*<hr />\s*<ol>\s*(.+)\s*</ol>\s*</div>#Us', array(&$this, '_makeFootnotes'), $html); + + return parent::parseString($html); + } + + /** + * replace HTML representation of footnotes with something more easily parsable + * + * @note this is a callback to be used in parseString() + * + * @param array $matches + * @return string + */ + protected function _makeFootnotes($matches) + { + // <li id="fn:1"> + // ... + // <a href="#fnref:block" rev="footnote">↩</a></p> + // </li> + // => <fn name="1">...</fn> + // remove footnote link + $fns = preg_replace('@\s*( \s*)?<a href="#fnref:[^"]+" rev="footnote"[^>]*>↩</a>\s*@s', '', $matches[1]); + // remove empty paragraph + $fns = preg_replace('@<p>\s*</p>@s', '', $fns); + // <li id="fn:1">...</li> -> <footnote nr="1">...</footnote> + $fns = str_replace('<li id="fn:', '<fn name="', $fns); + + $fns = '<footnotes>' . $fns . '</footnotes>'; + + return preg_replace('#</li>\s*(?=(?:<fn|</footnotes>))#s', '</fn>$1', $fns); + } + + /** + * handle <a> tags parsing + * + * @param void + * @return void + */ + protected function getCurrentCssSelector() + { + $cssSelector = ''; + if (isset($this->parser->tagAttributes['id'])) { + $cssSelector .= '#' . $this->decode($this->parser->tagAttributes['id']); + } + if (isset($this->parser->tagAttributes['class'])) { + $classes = explode(' ', $this->decode($this->parser->tagAttributes['class'])); + $classes = array_filter($classes); + $cssSelector .= '.' . join('.', $classes); + } + return $cssSelector; + } +} diff --git a/vendor/pixel418/markdownify/src/Parser.php b/vendor/pixel418/markdownify/src/Parser.php new file mode 100644 index 000000000..90fcdf9f8 --- /dev/null +++ b/vendor/pixel418/markdownify/src/Parser.php @@ -0,0 +1,564 @@ +<?php + +/* This file is part of the Markdownify project, which is under LGPL license */ + +namespace Markdownify; + +class Parser +{ + public static $skipWhitespace = true; + public static $a_ord; + public static $z_ord; + public static $special_ords; + + /** + * tags which are always empty (<br /> etc.) + * + * @var array<string> + */ + public $emptyTags = array( + 'br', + 'hr', + 'input', + 'img', + 'area', + 'link', + 'meta', + 'param', + ); + + /** + * tags with preformatted text + * whitespaces wont be touched in them + * + * @var array<string> + */ + public $preformattedTags = array( + 'script', + 'style', + 'pre', + 'code', + ); + + /** + * supress HTML tags inside preformatted tags (see above) + * + * @var bool + */ + public $noTagsInCode = false; + + /** + * html to be parsed + * + * @var string + */ + public $html = ''; + + /** + * node type: + * + * - tag (see isStartTag) + * - text (includes cdata) + * - comment + * - doctype + * - pi (processing instruction) + * + * @var string + */ + public $nodeType = ''; + + /** + * current node content, i.e. either a + * simple string (text node), or something like + * <tag attrib="value"...> + * + * @var string + */ + public $node = ''; + + /** + * wether current node is an opening tag (<a>) or not (</a>) + * set to NULL if current node is not a tag + * NOTE: empty tags (<br />) set this to true as well! + * + * @var bool | null + */ + public $isStartTag = null; + + /** + * wether current node is an empty tag (<br />) or not (<a></a>) + * + * @var bool | null + */ + public $isEmptyTag = null; + + /** + * tag name + * + * @var string | null + */ + public $tagName = ''; + + /** + * attributes of current tag + * + * @var array (attribName=>value) | null + */ + public $tagAttributes = null; + + /** + * whether or not the actual context is a inline context + * + * @var bool | null + */ + public $isInlineContext = null; + + /** + * whether the current tag is a block element + * + * @var bool | null + */ + public $isBlockElement = null; + + /** + * whether the previous tag (browser) is a block element + * + * @var bool | null + */ + public $isNextToInlineContext = null; + + /** + * keep whitespace + * + * @var int + */ + public $keepWhitespace = 0; + + /** + * list of open tags + * count this to get current depth + * + * @var array + */ + public $openTags = array(); + + /** + * list of block elements + * + * @var array + * TODO: what shall we do with <del> and <ins> ?! + */ + public $blockElements = array( + // tag name => <bool> is block + // block elements + 'address' => true, + 'blockquote' => true, + 'center' => true, + 'del' => true, + 'dir' => true, + 'div' => true, + 'dl' => true, + 'fieldset' => true, + 'form' => true, + 'h1' => true, + 'h2' => true, + 'h3' => true, + 'h4' => true, + 'h5' => true, + 'h6' => true, + 'hr' => true, + 'ins' => true, + 'isindex' => true, + 'menu' => true, + 'noframes' => true, + 'noscript' => true, + 'ol' => true, + 'p' => true, + 'pre' => true, + 'table' => true, + 'ul' => true, + // set table elements and list items to block as well + 'thead' => true, + 'tbody' => true, + 'tfoot' => true, + 'td' => true, + 'tr' => true, + 'th' => true, + 'li' => true, + 'dd' => true, + 'dt' => true, + // header items and html / body as well + 'html' => true, + 'body' => true, + 'head' => true, + 'meta' => true, + 'link' => true, + 'style' => true, + 'title' => true, + // unfancy media tags, when indented should be rendered as block + 'map' => true, + 'object' => true, + 'param' => true, + 'embed' => true, + 'area' => true, + // inline elements + 'a' => false, + 'abbr' => false, + 'acronym' => false, + 'applet' => false, + 'b' => false, + 'basefont' => false, + 'bdo' => false, + 'big' => false, + 'br' => false, + 'button' => false, + 'cite' => false, + 'code' => false, + 'del' => false, + 'dfn' => false, + 'em' => false, + 'font' => false, + 'i' => false, + 'img' => false, + 'ins' => false, + 'input' => false, + 'iframe' => false, + 'kbd' => false, + 'label' => false, + 'q' => false, + 'samp' => false, + 'script' => false, + 'select' => false, + 'small' => false, + 'span' => false, + 'strong' => false, + 'sub' => false, + 'sup' => false, + 'textarea' => false, + 'tt' => false, + 'var' => false, + ); + + /** + * get next node, set $this->html prior! + * + * @param void + * @return bool + */ + public function nextNode() + { + if (empty($this->html)) { + // we are done with parsing the html string + + return false; + } + + if ($this->isStartTag && !$this->isEmptyTag) { + array_push($this->openTags, $this->tagName); + if (in_array($this->tagName, $this->preformattedTags)) { + // dont truncate whitespaces for <code> or <pre> contents + $this->keepWhitespace++; + } + } + + if ($this->html[0] == '<') { + $token = substr($this->html, 0, 9); + if (substr($token, 0, 2) == '<?') { + // xml prolog or other pi's + /** TODO **/ + // trigger_error('this might need some work', E_USER_NOTICE); + $pos = strpos($this->html, '>'); + $this->setNode('pi', $pos + 1); + + return true; + } + if (substr($token, 0, 4) == '<!--') { + // comment + $pos = strpos($this->html, '-->'); + if ($pos === false) { + // could not find a closing -->, use next gt instead + // this is firefox' behaviour + $pos = strpos($this->html, '>') + 1; + } else { + $pos += 3; + } + $this->setNode('comment', $pos); + + static::$skipWhitespace = true; + + return true; + } + if ($token == '<!DOCTYPE') { + // doctype + $this->setNode('doctype', strpos($this->html, '>') + 1); + + static::$skipWhitespace = true; + + return true; + } + if ($token == '<![CDATA[') { + // cdata, use text node + + // remove leading <![CDATA[ + $this->html = substr($this->html, 9); + + $this->setNode('text', strpos($this->html, ']]>') + 3); + + // remove trailing ]]> and trim + $this->node = substr($this->node, 0, -3); + $this->handleWhitespaces(); + + static::$skipWhitespace = true; + + return true; + } + if ($this->parseTag()) { + // seems to be a tag + // handle whitespaces + if ($this->isBlockElement) { + static::$skipWhitespace = true; + } else { + static::$skipWhitespace = false; + } + + return true; + } + } + if ($this->keepWhitespace) { + static::$skipWhitespace = false; + } + // when we get here it seems to be a text node + $pos = strpos($this->html, '<'); + if ($pos === false) { + $pos = strlen($this->html); + } + $this->setNode('text', $pos); + $this->handleWhitespaces(); + if (static::$skipWhitespace && $this->node == ' ') { + return $this->nextNode(); + } + $this->isInlineContext = true; + static::$skipWhitespace = false; + + return true; + } + + /** + * parse tag, set tag name and attributes, see if it's a closing tag and so forth... + * + * @param void + * @return bool + */ + protected function parseTag() + { + if (!isset(static::$a_ord)) { + static::$a_ord = ord('a'); + static::$z_ord = ord('z'); + static::$special_ords = array( + ord(':'), // for xml:lang + ord('-'), // for http-equiv + ); + } + + $tagName = ''; + + $pos = 1; + $isStartTag = $this->html[$pos] != '/'; + if (!$isStartTag) { + $pos++; + } + // get tagName + while (isset($this->html[$pos])) { + $pos_ord = ord(strtolower($this->html[$pos])); + if (($pos_ord >= static::$a_ord && $pos_ord <= static::$z_ord) || (!empty($tagName) && is_numeric($this->html[$pos]))) { + $tagName .= $this->html[$pos]; + $pos++; + } else { + $pos--; + break; + } + } + + $tagName = strtolower($tagName); + if (empty($tagName) || !isset($this->blockElements[$tagName])) { + // something went wrong => invalid tag + $this->invalidTag(); + + return false; + } + if ($this->noTagsInCode && end($this->openTags) == 'code' && !($tagName == 'code' && !$isStartTag)) { + // we supress all HTML tags inside code tags + $this->invalidTag(); + + return false; + } + + // get tag attributes + /** TODO: in html 4 attributes do not need to be quoted **/ + $isEmptyTag = false; + $attributes = array(); + $currAttrib = ''; + while (isset($this->html[$pos + 1])) { + $pos++; + // close tag + if ($this->html[$pos] == '>' || $this->html[$pos] . $this->html[$pos + 1] == '/>') { + if ($this->html[$pos] == '/') { + $isEmptyTag = true; + $pos++; + } + break; + } + + $pos_ord = ord(strtolower($this->html[$pos])); + if (($pos_ord >= static::$a_ord && $pos_ord <= static::$z_ord) || in_array($pos_ord, static::$special_ords)) { + // attribute name + $currAttrib .= $this->html[$pos]; + } elseif (in_array($this->html[$pos], array(' ', "\t", "\n"))) { + // drop whitespace + } elseif (in_array($this->html[$pos] . $this->html[$pos + 1], array('="', "='"))) { + // get attribute value + $pos++; + $await = $this->html[$pos]; // single or double quote + $pos++; + $value = ''; + while (isset($this->html[$pos]) && $this->html[$pos] != $await) { + $value .= $this->html[$pos]; + $pos++; + } + $attributes[$currAttrib] = $value; + $currAttrib = ''; + } else { + $this->invalidTag(); + + return false; + } + } + if ($this->html[$pos] != '>') { + $this->invalidTag(); + + return false; + } + + if (!empty($currAttrib)) { + // html 4 allows something like <option selected> instead of <option selected="selected"> + $attributes[$currAttrib] = $currAttrib; + } + if (!$isStartTag) { + if (!empty($attributes) || $tagName != end($this->openTags)) { + // end tags must not contain any attributes + // or maybe we did not expect a different tag to be closed + $this->invalidTag(); + + return false; + } + array_pop($this->openTags); + if (in_array($tagName, $this->preformattedTags)) { + $this->keepWhitespace--; + } + } + $pos++; + $this->node = substr($this->html, 0, $pos); + $this->html = substr($this->html, $pos); + $this->tagName = $tagName; + $this->tagAttributes = $attributes; + $this->isStartTag = $isStartTag; + $this->isEmptyTag = $isEmptyTag || in_array($tagName, $this->emptyTags); + if ($this->isEmptyTag) { + // might be not well formed + $this->node = preg_replace('# */? *>$#', ' />', $this->node); + } + $this->nodeType = 'tag'; + $this->isBlockElement = $this->blockElements[$tagName]; + $this->isNextToInlineContext = $isStartTag && $this->isInlineContext; + $this->isInlineContext = !$this->isBlockElement; + return true; + } + + /** + * handle invalid tags + * + * @param void + * @return void + */ + protected function invalidTag() + { + $this->html = substr_replace($this->html, '<', 0, 1); + } + + /** + * update all vars and make $this->html shorter + * + * @param string $type see description for $this->nodeType + * @param int $pos to which position shall we cut? + * @return void + */ + protected function setNode($type, $pos) + { + if ($this->nodeType == 'tag') { + // set tag specific vars to null + // $type == tag should not be called here + // see this::parseTag() for more + $this->tagName = null; + $this->tagAttributes = null; + $this->isStartTag = null; + $this->isEmptyTag = null; + $this->isBlockElement = null; + + } + $this->nodeType = $type; + $this->node = substr($this->html, 0, $pos); + $this->html = substr($this->html, $pos); + } + + /** + * check if $this->html begins with $str + * + * @param string $str + * @return bool + */ + protected function match($str) + { + return substr($this->html, 0, strlen($str)) == $str; + } + + /** + * truncate whitespaces + * + * @param void + * @return void + */ + protected function handleWhitespaces() + { + if ($this->keepWhitespace) { + // <pre> or <code> before... + + return; + } + // truncate multiple whitespaces to a single one + $this->node = preg_replace('#\s+#s', ' ', $this->node); + } + + /** + * normalize self::node + * + * @param void + * @return void + */ + protected function normalizeNode() + { + $this->node = '<'; + if (!$this->isStartTag) { + $this->node .= '/' . $this->tagName . '>'; + + return; + } + $this->node .= $this->tagName; + foreach ($this->tagAttributes as $name => $value) { + $this->node .= ' ' . $name . '="' . str_replace('"', '"', $value) . '"'; + } + if ($this->isEmptyTag) { + $this->node .= ' /'; + } + $this->node .= '>'; + } +} diff --git a/vendor/simplepie/simplepie/LICENSE.txt b/vendor/simplepie/simplepie/LICENSE.txt new file mode 100644 index 000000000..a822a4bd9 --- /dev/null +++ b/vendor/simplepie/simplepie/LICENSE.txt @@ -0,0 +1,26 @@ +Copyright (c) 2004-2007, Ryan Parman and Geoffrey Sneddon. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + + * Neither the name of the SimplePie Team nor the names of its contributors may be used + to endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS +AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file diff --git a/vendor/simplepie/simplepie/README.markdown b/vendor/simplepie/simplepie/README.markdown new file mode 100644 index 000000000..c346204d0 --- /dev/null +++ b/vendor/simplepie/simplepie/README.markdown @@ -0,0 +1,113 @@ +SimplePie +========= + +SimplePie is a very fast and easy-to-use class, written in PHP, that puts the +'simple' back into 'really simple syndication'. Flexible enough to suit +beginners and veterans alike, SimplePie is focused on [speed, ease of use, +compatibility and standards compliance][what_is]. + +[what_is]: http://simplepie.org/wiki/faq/what_is_simplepie + + +Requirements +------------ +* PHP 5.3.0+ (5.3.6+ recommended since SimplePie 1.4.2) + * Support for PHP 5.2 stopped in branch `one-dot-three` + * Support for PHP 4 stopped in branch `one-dot-two` +* libxml2 (certain 2.7.x releases are too buggy for words, and will crash) +* One of iconv, mbstring or intl extensions +* cURL or fsockopen() +* PCRE support + + +What comes in the package? +-------------------------- +1. `library/` - SimplePie classes for use with the autoloader +2. `autoloader.php` - The SimplePie Autoloader if you want to use the separate + file version. +3. `README.markdown` - This document. +4. `LICENSE.txt` - A copy of the BSD license. +5. `compatibility_test/` - The SimplePie compatibility test that checks your + server for required settings. +6. `demo/` - A basic feed reader demo that shows off some of SimplePie's more + noticeable features. +7. `idn/` - A third-party library that SimplePie can optionally use to + understand Internationalized Domain Names (IDNs). +8. `build/` - Scripts related to generating pieces of SimplePie +9. `test/` - SimplePie's unit test suite. + +### Where's `simplepie.inc`? +Since SimplePie 1.3, we've split the classes into separate files to make it easier +to maintain and use. + +If you'd like a single monolithic file, you can run `php build/compile.php` to +generate `SimplePie.compiled.php`. + +To start the demo +----------------- +1. Upload this package to your webserver. +2. Make sure that the cache folder inside of the demo folder is server-writable. +3. Navigate your browser to the demo folder. + + +Need support? +------------- +For further setup and install documentation, function references, etc., visit +[the wiki][wiki]. If you're using the latest version off GitHub, you can also +check out the [API documentation][]. + +If you can't find an answer to your question in the documentation, head on over +to one of our [support channels][]. For bug reports and feature requests, visit +the [issue tracker][]. + +[API documentation]: http://dev.simplepie.org/api/ +[wiki]: http://simplepie.org/wiki/ +[support channels]: http://simplepie.org/support/ +[issue tracker]: http://github.com/simplepie/simplepie/issues + + +Project status +-------------- +SimplePie is currently maintained by Malcolm Blaney. + +As an open source project, SimplePie is maintained on a somewhat sporadic basis. +This means that feature requests may not be fulfilled straight away, as time has +to be prioritized. + +If you'd like to contribute to SimplePie, the best way to get started is to fork +the project on GitHub and send pull requests for patches. When doing so, please +be aware of our [coding standards][]. + +[coding standards]: http://simplepie.org/wiki/misc/coding_standards + + +Authors and contributors +------------------------ +### Current +* [Malcolm Blaney][] (Maintainer, support) + +### Alumni +* [Ryan McCue][] (developer, support) +* [Ryan Parman][] (Creator, developer, evangelism, support) +* [Geoffrey Sneddon][] (Lead developer) +* [Michael Shipley][] (Submitter of patches, support) +* [Steve Minutillo][] (Submitter of patches) + +[Malcolm Blaney]: https://unicyclic.com/mal +[Ryan McCue]: http://ryanmccue.info +[Ryan Parman]: http://ryanparman.com +[Geoffrey Sneddon]: http://gsnedders.com +[Michael Shipley]: http://michaelpshipley.com +[Steve Minutillo]: http://minutillo.com/steve/ + + +### Contributors +For a complete list of contributors: + +1. Pull down the latest SimplePie code +2. In the `simplepie` directory, run `git shortlog -ns` + + +License +------- +[New BSD license](http://www.opensource.org/licenses/BSD-3-Clause) diff --git a/vendor/simplepie/simplepie/autoloader.php b/vendor/simplepie/simplepie/autoloader.php new file mode 100644 index 000000000..fd7690da2 --- /dev/null +++ b/vendor/simplepie/simplepie/autoloader.php @@ -0,0 +1,85 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + + +// autoloader +spl_autoload_register(array(new SimplePie_Autoloader(), 'autoload')); + +if (!class_exists('SimplePie')) +{ + trigger_error('Autoloader not registered properly', E_USER_ERROR); +} + +/** + * Autoloader class + * + * @package SimplePie + * @subpackage API + */ +class SimplePie_Autoloader +{ + /** + * Constructor + */ + public function __construct() + { + $this->path = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'library'; + } + + /** + * Autoloader + * + * @param string $class The name of the class to attempt to load. + */ + public function autoload($class) + { + // Only load the class if it starts with "SimplePie" + if (strpos($class, 'SimplePie') !== 0) + { + return; + } + + $filename = $this->path . DIRECTORY_SEPARATOR . str_replace('_', DIRECTORY_SEPARATOR, $class) . '.php'; + include $filename; + } +}
\ No newline at end of file diff --git a/vendor/simplepie/simplepie/composer.json b/vendor/simplepie/simplepie/composer.json new file mode 100644 index 000000000..b5965b26f --- /dev/null +++ b/vendor/simplepie/simplepie/composer.json @@ -0,0 +1,40 @@ +{ + "name": "simplepie/simplepie", + "description": "A simple Atom/RSS parsing library for PHP", + "type": "library", + "keywords": ["rss", "atom", "feeds"], + "homepage": "http://simplepie.org/", + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Ryan Parman", + "homepage": "http://ryanparman.com/", + "role": "Creator, alumnus developer" + }, + { + "name": "Geoffrey Sneddon", + "homepage": "http://gsnedders.com/", + "role": "Alumnus developer" + }, + { + "name": "Ryan McCue", + "email": "me@ryanmccue.info", + "homepage": "http://ryanmccue.info/", + "role": "Developer" + } + ], + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "~4 || ~5" + }, + "suggest": { + "mf2/mf2": "Microformat module that allows for parsing HTML for microformats" + }, + "autoload": { + "psr-0": { + "SimplePie": "library" + } + } +} diff --git a/vendor/simplepie/simplepie/db.sql b/vendor/simplepie/simplepie/db.sql new file mode 100644 index 000000000..879382940 --- /dev/null +++ b/vendor/simplepie/simplepie/db.sql @@ -0,0 +1,40 @@ +/* SQLite */ +CREATE TABLE cache_data ( + id TEXT NOT NULL, + items SMALLINT NOT NULL DEFAULT 0, + data BLOB NOT NULL, + mtime INTEGER UNSIGNED NOT NULL +); +CREATE UNIQUE INDEX id ON cache_data(id); + +CREATE TABLE items ( + feed_id TEXT NOT NULL, + id TEXT NOT NULL, + data TEXT NOT NULL, + posted INTEGER UNSIGNED NOT NULL +); +CREATE INDEX feed_id ON items(feed_id); + + +/* MySQL */ +--- Don't paste this to create tables, since SimplePie will create +--- tables by its own. +CREATE TABLE `cache_data` ( + `id` TEXT CHARACTER SET utf8 NOT NULL, + `items` SMALLINT NOT NULL DEFAULT 0, + `data` BLOB NOT NULL, + `mtime` INT UNSIGNED NOT NULL, + UNIQUE ( + `id`(125) + ) +); + +CREATE TABLE `items` ( + `feed_id` TEXT CHARACTER SET utf8 NOT NULL, + `id` TEXT CHARACTER SET utf8 NOT NULL, + `data` MEDIUMBLOB NOT NULL, + `posted` INT UNSIGNED NOT NULL, + INDEX `feed_id` ( + `feed_id`(125) + ) +);
\ No newline at end of file diff --git a/vendor/simplepie/simplepie/idn/LICENCE b/vendor/simplepie/simplepie/idn/LICENCE new file mode 100644 index 000000000..25a1d22df --- /dev/null +++ b/vendor/simplepie/simplepie/idn/LICENCE @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/vendor/simplepie/simplepie/idn/ReadMe.txt b/vendor/simplepie/simplepie/idn/ReadMe.txt new file mode 100644 index 000000000..7ca8c7e6d --- /dev/null +++ b/vendor/simplepie/simplepie/idn/ReadMe.txt @@ -0,0 +1,123 @@ +******************************************************************************* +* * +* IDNA Convert (idna_convert.class.php) * +* * +* http://idnaconv.phlymail.de mailto:phlymail@phlylabs.de * +******************************************************************************* +* (c) 2004-2007 phlyLabs, Berlin * +* This file is encoded in UTF-8 * +******************************************************************************* + +Introduction +------------ + +The class idna_convert allows to convert internationalized domain names +(see RFC 3490, 3491, 3492 and 3454 for detials) as they can be used with various +registries worldwide to be translated between their original (localized) form +and their encoded form as it will be used in the DNS (Domain Name System). + +The class provides two public methods, encode() and decode(), which do exactly +what you would expect them to do. You are allowed to use complete domain names, +simple strings and complete email addresses as well. That means, that you might +use any of the following notations: + +- www.nörgler.com +- xn--nrgler-wxa +- xn--brse-5qa.xn--knrz-1ra.info + +Errors, incorrectly encoded or invalid strings will lead to either a FALSE +response (when in strict mode) or to only partially converted strings. +You can query the occured error by calling the method get_last_error(). + +Unicode strings are expected to be either UTF-8 strings, UCS-4 strings or UCS-4 +arrays. The default format is UTF-8. For setting different encodings, you can +call the method setParams() - please see the inline documentation for details. +ACE strings (the Punycode form) are always 7bit ASCII strings. + +ATTENTION: We no longer supply the PHP5 version of the class. It is not +necessary for achieving a successfull conversion, since the supplied PHP code is +compatible with both PHP4 and PHP5. We expect to see no compatibility issues +with the upcoming PHP6, too. + + +Files +----- + +idna_convert.class.php - The actual class +idna_convert.create.npdata.php - Useful for (re)creating the NPData file +npdata.ser - Serialized data for NamePrep +example.php - An example web page for converting +ReadMe.txt - This file +LICENCE - The LGPL licence file + +The class is contained in idna_convert.class.php. +MAKE SURE to copy the npdata.ser file into the same folder as the class file +itself! + + +Examples +-------- + +1. Say we wish to encode the domain name nörgler.com: + +// Include the class +include_once('idna_convert.class.php'); +// Instantiate it * +$IDN = new idna_convert(); +// The input string, if input is not UTF-8 or UCS-4, it must be converted before +$input = utf8_encode('nörgler.com'); +// Encode it to its punycode presentation +$output = $IDN->encode($input); +// Output, what we got now +echo $output; // This will read: xn--nrgler-wxa.com + + +2. We received an email from a punycoded domain and are willing to learn, how + the domain name reads originally + +// Include the class +include_once('idna_convert.class.php'); +// Instantiate it (depending on the version you are using) with +$IDN = new idna_convert(); +// The input string +$input = 'andre@xn--brse-5qa.xn--knrz-1ra.info'; +// Encode it to its punycode presentation +$output = $IDN->decode($input); +// Output, what we got now, if output should be in a format different to UTF-8 +// or UCS-4, you will have to convert it before outputting it +echo utf8_decode($output); // This will read: andre@börse.knörz.info + + +3. The input is read from a UCS-4 coded file and encoded line by line. By + appending the optional second parameter we tell enode() about the input + format to be used + +// Include the class +include_once('idna_convert.class.php'); +// Instantiate it +$IDN = new dinca_convert(); +// Iterate through the input file line by line +foreach (file('ucs4-domains.txt') as $line) { + echo $IDN->encode(trim($line), 'ucs4_string'); + echo "\n"; +} + + +NPData +------ + +Should you need to recreate the npdata.ser file, which holds all necessary translation +tables in a serialized format, you can run the file idna_convert.create.npdata.php, which +creates the file for you and stores it in the same folder, where it is placed. +Should you need to do changes to the tables you can do so, but beware of the consequences. + + +Contact us +---------- + +In case of errors, bugs, questions, wishes, please don't hesitate to contact us +under the email address above. + +The team of phlyLabs +http://phlylabs.de +mailto:phlymail@phlylabs.de
\ No newline at end of file diff --git a/vendor/simplepie/simplepie/idn/idna_convert.class.php b/vendor/simplepie/simplepie/idn/idna_convert.class.php new file mode 100644 index 000000000..eb9d5f516 --- /dev/null +++ b/vendor/simplepie/simplepie/idn/idna_convert.class.php @@ -0,0 +1,969 @@ +<?php +// {{{ license + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */ +// +// +----------------------------------------------------------------------+ +// | 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 | +// | USA. | +// +----------------------------------------------------------------------+ +// + +// }}} + +/** + * Encode/decode Internationalized Domain Names. + * + * The class allows to convert internationalized domain names + * (see RFC 3490 for details) as they can be used with various registries worldwide + * to be translated between their original (localized) form and their encoded form + * as it will be used in the DNS (Domain Name System). + * + * The class provides two public methods, encode() and decode(), which do exactly + * what you would expect them to do. You are allowed to use complete domain names, + * simple strings and complete email addresses as well. That means, that you might + * use any of the following notations: + * + * - www.nörgler.com + * - xn--nrgler-wxa + * - xn--brse-5qa.xn--knrz-1ra.info + * + * Unicode input might be given as either UTF-8 string, UCS-4 string or UCS-4 + * array. Unicode output is available in the same formats. + * You can select your preferred format via {@link set_paramter()}. + * + * ACE input and output is always expected to be ASCII. + * + * @author Matthias Sommerfeld <mso@phlylabs.de> + * @copyright 2004-2007 phlyLabs Berlin, http://phlylabs.de + * @version 0.5.1 + * + */ +class idna_convert +{ + /** + * Holds all relevant mapping tables, loaded from a seperate file on construct + * See RFC3454 for details + * + * @var array + * @access private + */ + var $NP = array(); + + // Internal settings, do not mess with them + var $_punycode_prefix = 'xn--'; + var $_invalid_ucs = 0x80000000; + var $_max_ucs = 0x10FFFF; + var $_base = 36; + var $_tmin = 1; + var $_tmax = 26; + var $_skew = 38; + var $_damp = 700; + var $_initial_bias = 72; + var $_initial_n = 0x80; + var $_sbase = 0xAC00; + var $_lbase = 0x1100; + var $_vbase = 0x1161; + var $_tbase = 0x11A7; + var $_lcount = 19; + var $_vcount = 21; + var $_tcount = 28; + var $_ncount = 588; // _vcount * _tcount + var $_scount = 11172; // _lcount * _tcount * _vcount + var $_error = false; + + // See {@link set_paramter()} for details of how to change the following + // settings from within your script / application + var $_api_encoding = 'utf8'; // Default input charset is UTF-8 + var $_allow_overlong = false; // Overlong UTF-8 encodings are forbidden + var $_strict_mode = false; // Behave strict or not + + // The constructor + function __construct($options = false) + { + $this->slast = $this->_sbase + $this->_lcount * $this->_vcount * $this->_tcount; + if (function_exists('file_get_contents')) { + $this->NP = unserialize(file_get_contents(dirname(__FILE__).'/npdata.ser')); + } else { + $this->NP = unserialize(join('', file(dirname(__FILE__).'/npdata.ser'))); + } + // If parameters are given, pass these to the respective method + if (is_array($options)) { + return $this->set_parameter($options); + } + return true; + } + + /** + * Sets a new option value. Available options and values: + * [encoding - Use either UTF-8, UCS4 as array or UCS4 as string as input ('utf8' for UTF-8, + * 'ucs4_string' and 'ucs4_array' respectively for UCS4); The output is always UTF-8] + * [overlong - Unicode does not allow unnecessarily long encodings of chars, + * to allow this, set this parameter to true, else to false; + * default is false.] + * [strict - true: strict mode, good for registration purposes - Causes errors + * on failures; false: loose mode, ideal for "wildlife" applications + * by silently ignoring errors and returning the original input instead + * + * @param mixed Parameter to set (string: single parameter; array of Parameter => Value pairs) + * @param string Value to use (if parameter 1 is a string) + * @return boolean true on success, false otherwise + * @access public + */ + function set_parameter($option, $value = false) + { + if (!is_array($option)) { + $option = array($option => $value); + } + foreach ($option as $k => $v) { + switch ($k) { + case 'encoding': + switch ($v) { + case 'utf8': + case 'ucs4_string': + case 'ucs4_array': + $this->_api_encoding = $v; + break; + default: + $this->_error('Set Parameter: Unknown parameter '.$v.' for option '.$k); + return false; + } + break; + case 'overlong': + $this->_allow_overlong = ($v) ? true : false; + break; + case 'strict': + $this->_strict_mode = ($v) ? true : false; + break; + default: + $this->_error('Set Parameter: Unknown option '.$k); + return false; + } + } + return true; + } + + /** + * Decode a given ACE domain name + * @param string Domain name (ACE string) + * [@param string Desired output encoding, see {@link set_parameter}] + * @return string Decoded Domain name (UTF-8 or UCS-4) + * @access public + */ + function decode($input, $one_time_encoding = false) + { + // Optionally set + if ($one_time_encoding) { + switch ($one_time_encoding) { + case 'utf8': + case 'ucs4_string': + case 'ucs4_array': + break; + default: + $this->_error('Unknown encoding '.$one_time_encoding); + return false; + } + } + // Make sure to drop any newline characters around + $input = trim($input); + + // Negotiate input and try to determine, whether it is a plain string, + // an email address or something like a complete URL + if (strpos($input, '@')) { // Maybe it is an email address + // No no in strict mode + if ($this->_strict_mode) { + $this->_error('Only simple domain name parts can be handled in strict mode'); + return false; + } + list ($email_pref, $input) = explode('@', $input, 2); + $arr = explode('.', $input); + foreach ($arr as $k => $v) { + if (preg_match('!^'.preg_quote($this->_punycode_prefix, '!').'!', $v)) { + $conv = $this->_decode($v); + if ($conv) $arr[$k] = $conv; + } + } + $input = join('.', $arr); + $arr = explode('.', $email_pref); + foreach ($arr as $k => $v) { + if (preg_match('!^'.preg_quote($this->_punycode_prefix, '!').'!', $v)) { + $conv = $this->_decode($v); + if ($conv) $arr[$k] = $conv; + } + } + $email_pref = join('.', $arr); + $return = $email_pref . '@' . $input; + } elseif (preg_match('![:\./]!', $input)) { // Or a complete domain name (with or without paths / parameters) + // No no in strict mode + if ($this->_strict_mode) { + $this->_error('Only simple domain name parts can be handled in strict mode'); + return false; + } + $parsed = parse_url($input); + if (isset($parsed['host'])) { + $arr = explode('.', $parsed['host']); + foreach ($arr as $k => $v) { + $conv = $this->_decode($v); + if ($conv) $arr[$k] = $conv; + } + $parsed['host'] = join('.', $arr); + $return = + (empty($parsed['scheme']) ? '' : $parsed['scheme'].(strtolower($parsed['scheme']) == 'mailto' ? ':' : '://')) + .(empty($parsed['user']) ? '' : $parsed['user'].(empty($parsed['pass']) ? '' : ':'.$parsed['pass']).'@') + .$parsed['host'] + .(empty($parsed['port']) ? '' : ':'.$parsed['port']) + .(empty($parsed['path']) ? '' : $parsed['path']) + .(empty($parsed['query']) ? '' : '?'.$parsed['query']) + .(empty($parsed['fragment']) ? '' : '#'.$parsed['fragment']); + } else { // parse_url seems to have failed, try without it + $arr = explode('.', $input); + foreach ($arr as $k => $v) { + $conv = $this->_decode($v); + $arr[$k] = ($conv) ? $conv : $v; + } + $return = join('.', $arr); + } + } else { // Otherwise we consider it being a pure domain name string + $return = $this->_decode($input); + if (!$return) $return = $input; + } + // The output is UTF-8 by default, other output formats need conversion here + // If one time encoding is given, use this, else the objects property + switch (($one_time_encoding) ? $one_time_encoding : $this->_api_encoding) { + case 'utf8': + return $return; + break; + case 'ucs4_string': + return $this->_ucs4_to_ucs4_string($this->_utf8_to_ucs4($return)); + break; + case 'ucs4_array': + return $this->_utf8_to_ucs4($return); + break; + default: + $this->_error('Unsupported output format'); + return false; + } + } + + /** + * Encode a given UTF-8 domain name + * @param string Domain name (UTF-8 or UCS-4) + * [@param string Desired input encoding, see {@link set_parameter}] + * @return string Encoded Domain name (ACE string) + * @access public + */ + function encode($decoded, $one_time_encoding = false) + { + // Forcing conversion of input to UCS4 array + // If one time encoding is given, use this, else the objects property + switch ($one_time_encoding ? $one_time_encoding : $this->_api_encoding) { + case 'utf8': + $decoded = $this->_utf8_to_ucs4($decoded); + break; + case 'ucs4_string': + $decoded = $this->_ucs4_string_to_ucs4($decoded); + case 'ucs4_array': + break; + default: + $this->_error('Unsupported input format: '.($one_time_encoding ? $one_time_encoding : $this->_api_encoding)); + return false; + } + + // No input, no output, what else did you expect? + if (empty($decoded)) return ''; + + // Anchors for iteration + $last_begin = 0; + // Output string + $output = ''; + foreach ($decoded as $k => $v) { + // Make sure to use just the plain dot + switch($v) { + case 0x3002: + case 0xFF0E: + case 0xFF61: + $decoded[$k] = 0x2E; + // Right, no break here, the above are converted to dots anyway + // Stumbling across an anchoring character + case 0x2E: + case 0x2F: + case 0x3A: + case 0x3F: + case 0x40: + // Neither email addresses nor URLs allowed in strict mode + if ($this->_strict_mode) { + $this->_error('Neither email addresses nor URLs are allowed in strict mode.'); + return false; + } else { + // Skip first char + if ($k) { + $encoded = ''; + $encoded = $this->_encode(array_slice($decoded, $last_begin, (($k)-$last_begin))); + if ($encoded) { + $output .= $encoded; + } else { + $output .= $this->_ucs4_to_utf8(array_slice($decoded, $last_begin, (($k)-$last_begin))); + } + $output .= chr($decoded[$k]); + } + $last_begin = $k + 1; + } + } + } + // Catch the rest of the string + if ($last_begin) { + $inp_len = sizeof($decoded); + $encoded = ''; + $encoded = $this->_encode(array_slice($decoded, $last_begin, (($inp_len)-$last_begin))); + if ($encoded) { + $output .= $encoded; + } else { + $output .= $this->_ucs4_to_utf8(array_slice($decoded, $last_begin, (($inp_len)-$last_begin))); + } + return $output; + } else { + if ($output = $this->_encode($decoded)) { + return $output; + } else { + return $this->_ucs4_to_utf8($decoded); + } + } + } + + /** + * Use this method to get the last error ocurred + * @param void + * @return string The last error, that occured + * @access public + */ + function get_last_error() + { + return $this->_error; + } + + /** + * The actual decoding algorithm + * @access private + */ + function _decode($encoded) + { + // We do need to find the Punycode prefix + if (!preg_match('!^'.preg_quote($this->_punycode_prefix, '!').'!', $encoded)) { + $this->_error('This is not a punycode string'); + return false; + } + $encode_test = preg_replace('!^'.preg_quote($this->_punycode_prefix, '!').'!', '', $encoded); + // If nothing left after removing the prefix, it is hopeless + if (!$encode_test) { + $this->_error('The given encoded string was empty'); + return false; + } + // Find last occurence of the delimiter + $delim_pos = strrpos($encoded, '-'); + if ($delim_pos > strlen($this->_punycode_prefix)) { + for ($k = strlen($this->_punycode_prefix); $k < $delim_pos; ++$k) { + $decoded[] = ord($encoded{$k}); + } + } else { + $decoded = array(); + } + $deco_len = count($decoded); + $enco_len = strlen($encoded); + + // Wandering through the strings; init + $is_first = true; + $bias = $this->_initial_bias; + $idx = 0; + $char = $this->_initial_n; + + for ($enco_idx = ($delim_pos) ? ($delim_pos + 1) : 0; $enco_idx < $enco_len; ++$deco_len) { + for ($old_idx = $idx, $w = 1, $k = $this->_base; 1 ; $k += $this->_base) { + $digit = $this->_decode_digit($encoded{$enco_idx++}); + $idx += $digit * $w; + $t = ($k <= $bias) ? $this->_tmin : + (($k >= $bias + $this->_tmax) ? $this->_tmax : ($k - $bias)); + if ($digit < $t) break; + $w = (int) ($w * ($this->_base - $t)); + } + $bias = $this->_adapt($idx - $old_idx, $deco_len + 1, $is_first); + $is_first = false; + $char += (int) ($idx / ($deco_len + 1)); + $idx %= ($deco_len + 1); + if ($deco_len > 0) { + // Make room for the decoded char + for ($i = $deco_len; $i > $idx; $i--) { + $decoded[$i] = $decoded[($i - 1)]; + } + } + $decoded[$idx++] = $char; + } + return $this->_ucs4_to_utf8($decoded); + } + + /** + * The actual encoding algorithm + * @access private + */ + function _encode($decoded) + { + // We cannot encode a domain name containing the Punycode prefix + $extract = strlen($this->_punycode_prefix); + $check_pref = $this->_utf8_to_ucs4($this->_punycode_prefix); + $check_deco = array_slice($decoded, 0, $extract); + + if ($check_pref == $check_deco) { + $this->_error('This is already a punycode string'); + return false; + } + // We will not try to encode strings consisting of basic code points only + $encodable = false; + foreach ($decoded as $k => $v) { + if ($v > 0x7a) { + $encodable = true; + break; + } + } + if (!$encodable) { + $this->_error('The given string does not contain encodable chars'); + return false; + } + + // Do NAMEPREP + $decoded = $this->_nameprep($decoded); + if (!$decoded || !is_array($decoded)) return false; // NAMEPREP failed + + $deco_len = count($decoded); + if (!$deco_len) return false; // Empty array + + $codecount = 0; // How many chars have been consumed + + $encoded = ''; + // Copy all basic code points to output + for ($i = 0; $i < $deco_len; ++$i) { + $test = $decoded[$i]; + // Will match [-0-9a-zA-Z] + if ((0x2F < $test && $test < 0x40) || (0x40 < $test && $test < 0x5B) + || (0x60 < $test && $test <= 0x7B) || (0x2D == $test)) { + $encoded .= chr($decoded[$i]); + $codecount++; + } + } + if ($codecount == $deco_len) return $encoded; // All codepoints were basic ones + + // Start with the prefix; copy it to output + $encoded = $this->_punycode_prefix.$encoded; + + // If we have basic code points in output, add an hyphen to the end + if ($codecount) $encoded .= '-'; + + // Now find and encode all non-basic code points + $is_first = true; + $cur_code = $this->_initial_n; + $bias = $this->_initial_bias; + $delta = 0; + while ($codecount < $deco_len) { + // Find the smallest code point >= the current code point and + // remember the last ouccrence of it in the input + for ($i = 0, $next_code = $this->_max_ucs; $i < $deco_len; $i++) { + if ($decoded[$i] >= $cur_code && $decoded[$i] <= $next_code) { + $next_code = $decoded[$i]; + } + } + + $delta += ($next_code - $cur_code) * ($codecount + 1); + $cur_code = $next_code; + + // Scan input again and encode all characters whose code point is $cur_code + for ($i = 0; $i < $deco_len; $i++) { + if ($decoded[$i] < $cur_code) { + $delta++; + } elseif ($decoded[$i] == $cur_code) { + for ($q = $delta, $k = $this->_base; 1; $k += $this->_base) { + $t = ($k <= $bias) ? $this->_tmin : + (($k >= $bias + $this->_tmax) ? $this->_tmax : $k - $bias); + if ($q < $t) break; + $encoded .= $this->_encode_digit(intval($t + (($q - $t) % ($this->_base - $t)))); //v0.4.5 Changed from ceil() to intval() + $q = (int) (($q - $t) / ($this->_base - $t)); + } + $encoded .= $this->_encode_digit($q); + $bias = $this->_adapt($delta, $codecount+1, $is_first); + $codecount++; + $delta = 0; + $is_first = false; + } + } + $delta++; + $cur_code++; + } + return $encoded; + } + + /** + * Adapt the bias according to the current code point and position + * @access private + */ + function _adapt($delta, $npoints, $is_first) + { + $delta = intval($is_first ? ($delta / $this->_damp) : ($delta / 2)); + $delta += intval($delta / $npoints); + for ($k = 0; $delta > (($this->_base - $this->_tmin) * $this->_tmax) / 2; $k += $this->_base) { + $delta = intval($delta / ($this->_base - $this->_tmin)); + } + return intval($k + ($this->_base - $this->_tmin + 1) * $delta / ($delta + $this->_skew)); + } + + /** + * Encoding a certain digit + * @access private + */ + function _encode_digit($d) + { + return chr($d + 22 + 75 * ($d < 26)); + } + + /** + * Decode a certain digit + * @access private + */ + function _decode_digit($cp) + { + $cp = ord($cp); + return ($cp - 48 < 10) ? $cp - 22 : (($cp - 65 < 26) ? $cp - 65 : (($cp - 97 < 26) ? $cp - 97 : $this->_base)); + } + + /** + * Internal error handling method + * @access private + */ + function _error($error = '') + { + $this->_error = $error; + } + + /** + * Do Nameprep according to RFC3491 and RFC3454 + * @param array Unicode Characters + * @return string Unicode Characters, Nameprep'd + * @access private + */ + function _nameprep($input) + { + $output = array(); + $error = false; + // + // Mapping + // Walking through the input array, performing the required steps on each of + // the input chars and putting the result into the output array + // While mapping required chars we apply the cannonical ordering + foreach ($input as $v) { + // Map to nothing == skip that code point + if (in_array($v, $this->NP['map_nothing'])) continue; + + // Try to find prohibited input + if (in_array($v, $this->NP['prohibit']) || in_array($v, $this->NP['general_prohibited'])) { + $this->_error('NAMEPREP: Prohibited input U+'.sprintf('%08X', $v)); + return false; + } + foreach ($this->NP['prohibit_ranges'] as $range) { + if ($range[0] <= $v && $v <= $range[1]) { + $this->_error('NAMEPREP: Prohibited input U+'.sprintf('%08X', $v)); + return false; + } + } + // + // Hangul syllable decomposition + if (0xAC00 <= $v && $v <= 0xD7AF) { + foreach ($this->_hangul_decompose($v) as $out) { + $output[] = (int) $out; + } + // There's a decomposition mapping for that code point + } elseif (isset($this->NP['replacemaps'][$v])) { + foreach ($this->_apply_cannonical_ordering($this->NP['replacemaps'][$v]) as $out) { + $output[] = (int) $out; + } + } else { + $output[] = (int) $v; + } + } + // Before applying any Combining, try to rearrange any Hangul syllables + $output = $this->_hangul_compose($output); + // + // Combine code points + // + $last_class = 0; + $last_starter = 0; + $out_len = count($output); + for ($i = 0; $i < $out_len; ++$i) { + $class = $this->_get_combining_class($output[$i]); + if ((!$last_class || $last_class > $class) && $class) { + // Try to match + $seq_len = $i - $last_starter; + $out = $this->_combine(array_slice($output, $last_starter, $seq_len)); + // On match: Replace the last starter with the composed character and remove + // the now redundant non-starter(s) + if ($out) { + $output[$last_starter] = $out; + if (count($out) != $seq_len) { + for ($j = $i+1; $j < $out_len; ++$j) { + $output[$j-1] = $output[$j]; + } + unset($output[$out_len]); + } + // Rewind the for loop by one, since there can be more possible compositions + $i--; + $out_len--; + $last_class = ($i == $last_starter) ? 0 : $this->_get_combining_class($output[$i-1]); + continue; + } + } + // The current class is 0 + if (!$class) $last_starter = $i; + $last_class = $class; + } + return $output; + } + + /** + * Decomposes a Hangul syllable + * (see http://www.unicode.org/unicode/reports/tr15/#Hangul + * @param integer 32bit UCS4 code point + * @return array Either Hangul Syllable decomposed or original 32bit value as one value array + * @access private + */ + function _hangul_decompose($char) + { + $sindex = (int) $char - $this->_sbase; + if ($sindex < 0 || $sindex >= $this->_scount) { + return array($char); + } + $result = array(); + $result[] = (int) $this->_lbase + $sindex / $this->_ncount; + $result[] = (int) $this->_vbase + ($sindex % $this->_ncount) / $this->_tcount; + $T = intval($this->_tbase + $sindex % $this->_tcount); + if ($T != $this->_tbase) $result[] = $T; + return $result; + } + /** + * Ccomposes a Hangul syllable + * (see http://www.unicode.org/unicode/reports/tr15/#Hangul + * @param array Decomposed UCS4 sequence + * @return array UCS4 sequence with syllables composed + * @access private + */ + function _hangul_compose($input) + { + $inp_len = count($input); + if (!$inp_len) return array(); + $result = array(); + $last = (int) $input[0]; + $result[] = $last; // copy first char from input to output + + for ($i = 1; $i < $inp_len; ++$i) { + $char = (int) $input[$i]; + $sindex = $last - $this->_sbase; + $lindex = $last - $this->_lbase; + $vindex = $char - $this->_vbase; + $tindex = $char - $this->_tbase; + // Find out, whether two current characters are LV and T + if (0 <= $sindex && $sindex < $this->_scount && ($sindex % $this->_tcount == 0) + && 0 <= $tindex && $tindex <= $this->_tcount) { + // create syllable of form LVT + $last += $tindex; + $result[(count($result) - 1)] = $last; // reset last + continue; // discard char + } + // Find out, whether two current characters form L and V + if (0 <= $lindex && $lindex < $this->_lcount && 0 <= $vindex && $vindex < $this->_vcount) { + // create syllable of form LV + $last = (int) $this->_sbase + ($lindex * $this->_vcount + $vindex) * $this->_tcount; + $result[(count($result) - 1)] = $last; // reset last + continue; // discard char + } + // if neither case was true, just add the character + $last = $char; + $result[] = $char; + } + return $result; + } + + /** + * Returns the combining class of a certain wide char + * @param integer Wide char to check (32bit integer) + * @return integer Combining class if found, else 0 + * @access private + */ + function _get_combining_class($char) + { + return isset($this->NP['norm_combcls'][$char]) ? $this->NP['norm_combcls'][$char] : 0; + } + + /** + * Apllies the cannonical ordering of a decomposed UCS4 sequence + * @param array Decomposed UCS4 sequence + * @return array Ordered USC4 sequence + * @access private + */ + function _apply_cannonical_ordering($input) + { + $swap = true; + $size = count($input); + while ($swap) { + $swap = false; + $last = $this->_get_combining_class(intval($input[0])); + for ($i = 0; $i < $size-1; ++$i) { + $next = $this->_get_combining_class(intval($input[$i+1])); + if ($next != 0 && $last > $next) { + // Move item leftward until it fits + for ($j = $i + 1; $j > 0; --$j) { + if ($this->_get_combining_class(intval($input[$j-1])) <= $next) break; + $t = intval($input[$j]); + $input[$j] = intval($input[$j-1]); + $input[$j-1] = $t; + $swap = true; + } + // Reentering the loop looking at the old character again + $next = $last; + } + $last = $next; + } + } + return $input; + } + + /** + * Do composition of a sequence of starter and non-starter + * @param array UCS4 Decomposed sequence + * @return array Ordered USC4 sequence + * @access private + */ + function _combine($input) + { + $inp_len = count($input); + foreach ($this->NP['replacemaps'] as $np_src => $np_target) { + if ($np_target[0] != $input[0]) continue; + if (count($np_target) != $inp_len) continue; + $hit = false; + foreach ($input as $k2 => $v2) { + if ($v2 == $np_target[$k2]) { + $hit = true; + } else { + $hit = false; + break; + } + } + if ($hit) return $np_src; + } + return false; + } + + /** + * This converts an UTF-8 encoded string to its UCS-4 representation + * By talking about UCS-4 "strings" we mean arrays of 32bit integers representing + * each of the "chars". This is due to PHP not being able to handle strings with + * bit depth different from 8. This apllies to the reverse method _ucs4_to_utf8(), too. + * The following UTF-8 encodings are supported: + * bytes bits representation + * 1 7 0xxxxxxx + * 2 11 110xxxxx 10xxxxxx + * 3 16 1110xxxx 10xxxxxx 10xxxxxx + * 4 21 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + * 5 26 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + * 6 31 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + * Each x represents a bit that can be used to store character data. + * The five and six byte sequences are part of Annex D of ISO/IEC 10646-1:2000 + * @access private + */ + function _utf8_to_ucs4($input) + { + $output = array(); + $out_len = 0; + $inp_len = strlen($input); + $mode = 'next'; + $test = 'none'; + for ($k = 0; $k < $inp_len; ++$k) { + $v = ord($input{$k}); // Extract byte from input string + + if ($v < 128) { // We found an ASCII char - put into stirng as is + $output[$out_len] = $v; + ++$out_len; + if ('add' == $mode) { + $this->_error('Conversion from UTF-8 to UCS-4 failed: malformed input at byte '.$k); + return false; + } + continue; + } + if ('next' == $mode) { // Try to find the next start byte; determine the width of the Unicode char + $start_byte = $v; + $mode = 'add'; + $test = 'range'; + if ($v >> 5 == 6) { // &110xxxxx 10xxxxx + $next_byte = 0; // Tells, how many times subsequent bitmasks must rotate 6bits to the left + $v = ($v - 192) << 6; + } elseif ($v >> 4 == 14) { // &1110xxxx 10xxxxxx 10xxxxxx + $next_byte = 1; + $v = ($v - 224) << 12; + } elseif ($v >> 3 == 30) { // &11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + $next_byte = 2; + $v = ($v - 240) << 18; + } elseif ($v >> 2 == 62) { // &111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + $next_byte = 3; + $v = ($v - 248) << 24; + } elseif ($v >> 1 == 126) { // &1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + $next_byte = 4; + $v = ($v - 252) << 30; + } else { + $this->_error('This might be UTF-8, but I don\'t understand it at byte '.$k); + return false; + } + if ('add' == $mode) { + $output[$out_len] = (int) $v; + ++$out_len; + continue; + } + } + if ('add' == $mode) { + if (!$this->_allow_overlong && $test == 'range') { + $test = 'none'; + if (($v < 0xA0 && $start_byte == 0xE0) || ($v < 0x90 && $start_byte == 0xF0) || ($v > 0x8F && $start_byte == 0xF4)) { + $this->_error('Bogus UTF-8 character detected (out of legal range) at byte '.$k); + return false; + } + } + if ($v >> 6 == 2) { // Bit mask must be 10xxxxxx + $v = ($v - 128) << ($next_byte * 6); + $output[($out_len - 1)] += $v; + --$next_byte; + } else { + $this->_error('Conversion from UTF-8 to UCS-4 failed: malformed input at byte '.$k); + return false; + } + if ($next_byte < 0) { + $mode = 'next'; + } + } + } // for + return $output; + } + + /** + * Convert UCS-4 string into UTF-8 string + * See _utf8_to_ucs4() for details + * @access private + */ + function _ucs4_to_utf8($input) + { + $output = ''; + $k = 0; + foreach ($input as $v) { + ++$k; + // $v = ord($v); + if ($v < 128) { // 7bit are transferred literally + $output .= chr($v); + } elseif ($v < (1 << 11)) { // 2 bytes + $output .= chr(192 + ($v >> 6)) . chr(128 + ($v & 63)); + } elseif ($v < (1 << 16)) { // 3 bytes + $output .= chr(224 + ($v >> 12)) . chr(128 + (($v >> 6) & 63)) . chr(128 + ($v & 63)); + } elseif ($v < (1 << 21)) { // 4 bytes + $output .= chr(240 + ($v >> 18)) . chr(128 + (($v >> 12) & 63)) + . chr(128 + (($v >> 6) & 63)) . chr(128 + ($v & 63)); + } elseif ($v < (1 << 26)) { // 5 bytes + $output .= chr(248 + ($v >> 24)) . chr(128 + (($v >> 18) & 63)) + . chr(128 + (($v >> 12) & 63)) . chr(128 + (($v >> 6) & 63)) + . chr(128 + ($v & 63)); + } elseif ($v < (1 << 31)) { // 6 bytes + $output .= chr(252 + ($v >> 30)) . chr(128 + (($v >> 24) & 63)) + . chr(128 + (($v >> 18) & 63)) . chr(128 + (($v >> 12) & 63)) + . chr(128 + (($v >> 6) & 63)) . chr(128 + ($v & 63)); + } else { + $this->_error('Conversion from UCS-4 to UTF-8 failed: malformed input at byte '.$k); + return false; + } + } + return $output; + } + + /** + * Convert UCS-4 array into UCS-4 string + * + * @access private + */ + function _ucs4_to_ucs4_string($input) + { + $output = ''; + // Take array values and split output to 4 bytes per value + // The bit mask is 255, which reads &11111111 + foreach ($input as $v) { + $output .= chr(($v >> 24) & 255).chr(($v >> 16) & 255).chr(($v >> 8) & 255).chr($v & 255); + } + return $output; + } + + /** + * Convert UCS-4 strin into UCS-4 garray + * + * @access private + */ + function _ucs4_string_to_ucs4($input) + { + $output = array(); + $inp_len = strlen($input); + // Input length must be dividable by 4 + if ($inp_len % 4) { + $this->_error('Input UCS4 string is broken'); + return false; + } + // Empty input - return empty output + if (!$inp_len) return $output; + for ($i = 0, $out_len = -1; $i < $inp_len; ++$i) { + // Increment output position every 4 input bytes + if (!($i % 4)) { + $out_len++; + $output[$out_len] = 0; + } + $output[$out_len] += ord($input{$i}) << (8 * (3 - ($i % 4) ) ); + } + return $output; + } +} + +/** +* Adapter class for aligning the API of idna_convert with that of Net_IDNA +* @author Matthias Sommerfeld <mso@phlylabs.de> +*/ +class Net_IDNA_php4 extends idna_convert +{ + /** + * Sets a new option value. Available options and values: + * [encoding - Use either UTF-8, UCS4 as array or UCS4 as string as input ('utf8' for UTF-8, + * 'ucs4_string' and 'ucs4_array' respectively for UCS4); The output is always UTF-8] + * [overlong - Unicode does not allow unnecessarily long encodings of chars, + * to allow this, set this parameter to true, else to false; + * default is false.] + * [strict - true: strict mode, good for registration purposes - Causes errors + * on failures; false: loose mode, ideal for "wildlife" applications + * by silently ignoring errors and returning the original input instead + * + * @param mixed Parameter to set (string: single parameter; array of Parameter => Value pairs) + * @param string Value to use (if parameter 1 is a string) + * @return boolean true on success, false otherwise + * @access public + */ + function setParams($option, $param = false) + { + return $this->IC->set_parameters($option, $param); + } +} + +?> diff --git a/vendor/simplepie/simplepie/idn/npdata.ser b/vendor/simplepie/simplepie/idn/npdata.ser new file mode 100644 index 000000000..d7ce6d03f --- /dev/null +++ b/vendor/simplepie/simplepie/idn/npdata.ser @@ -0,0 +1 @@ +a:6:{s:11:"map_nothing";a:27:{i:0;i:173;i:1;i:847;i:2;i:6150;i:3;i:6155;i:4;i:6156;i:5;i:6157;i:6;i:8203;i:7;i:8204;i:8;i:8205;i:9;i:8288;i:10;i:65024;i:11;i:65025;i:12;i:65026;i:13;i:65027;i:14;i:65028;i:15;i:65029;i:16;i:65030;i:17;i:65031;i:18;i:65032;i:19;i:65033;i:20;i:65034;i:21;i:65035;i:22;i:65036;i:23;i:65037;i:24;i:65038;i:25;i:65039;i:26;i:65279;}s:18:"general_prohibited";a:64:{i:0;i:0;i:1;i:1;i:2;i:2;i:3;i:3;i:4;i:4;i:5;i:5;i:6;i:6;i:7;i:7;i:8;i:8;i:9;i:9;i:10;i:10;i:11;i:11;i:12;i:12;i:13;i:13;i:14;i:14;i:15;i:15;i:16;i:16;i:17;i:17;i:18;i:18;i:19;i:19;i:20;i:20;i:21;i:21;i:22;i:22;i:23;i:23;i:24;i:24;i:25;i:25;i:26;i:26;i:27;i:27;i:28;i:28;i:29;i:29;i:30;i:30;i:31;i:31;i:32;i:32;i:33;i:33;i:34;i:34;i:35;i:35;i:36;i:36;i:37;i:37;i:38;i:38;i:39;i:39;i:40;i:40;i:41;i:41;i:42;i:42;i:43;i:43;i:44;i:44;i:45;i:47;i:46;i:59;i:47;i:60;i:48;i:61;i:49;i:62;i:50;i:63;i:51;i:64;i:52;i:91;i:53;i:92;i:54;i:93;i:55;i:94;i:56;i:95;i:57;i:96;i:58;i:123;i:59;i:124;i:60;i:125;i:61;i:126;i:62;i:127;i:63;i:12290;}s:8:"prohibit";a:84:{i:0;i:160;i:1;i:5760;i:2;i:8192;i:3;i:8193;i:4;i:8194;i:5;i:8195;i:6;i:8196;i:7;i:8197;i:8;i:8198;i:9;i:8199;i:10;i:8200;i:11;i:8201;i:12;i:8202;i:13;i:8203;i:14;i:8239;i:15;i:8287;i:16;i:12288;i:17;i:1757;i:18;i:1807;i:19;i:6158;i:20;i:8204;i:21;i:8205;i:22;i:8232;i:23;i:8233;i:24;i:65279;i:25;i:65529;i:26;i:65530;i:27;i:65531;i:28;i:65532;i:29;i:65534;i:30;i:65535;i:31;i:131070;i:32;i:131071;i:33;i:196606;i:34;i:196607;i:35;i:262142;i:36;i:262143;i:37;i:327678;i:38;i:327679;i:39;i:393214;i:40;i:393215;i:41;i:458750;i:42;i:458751;i:43;i:524286;i:44;i:524287;i:45;i:589822;i:46;i:589823;i:47;i:655358;i:48;i:655359;i:49;i:720894;i:50;i:720895;i:51;i:786430;i:52;i:786431;i:53;i:851966;i:54;i:851967;i:55;i:917502;i:56;i:917503;i:57;i:983038;i:58;i:983039;i:59;i:1048574;i:60;i:1048575;i:61;i:1114110;i:62;i:1114111;i:63;i:65529;i:64;i:65530;i:65;i:65531;i:66;i:65532;i:67;i:65533;i:68;i:832;i:69;i:833;i:70;i:8206;i:71;i:8207;i:72;i:8234;i:73;i:8235;i:74;i:8236;i:75;i:8237;i:76;i:8238;i:77;i:8298;i:78;i:8299;i:79;i:8300;i:80;i:8301;i:81;i:8302;i:82;i:8303;i:83;i:917505;}s:15:"prohibit_ranges";a:10:{i:0;a:2:{i:0;i:128;i:1;i:159;}i:1;a:2:{i:0;i:8288;i:1;i:8303;}i:2;a:2:{i:0;i:119155;i:1;i:119162;}i:3;a:2:{i:0;i:57344;i:1;i:63743;}i:4;a:2:{i:0;i:983040;i:1;i:1048573;}i:5;a:2:{i:0;i:1048576;i:1;i:1114109;}i:6;a:2:{i:0;i:64976;i:1;i:65007;}i:7;a:2:{i:0;i:55296;i:1;i:57343;}i:8;a:2:{i:0;i:12272;i:1;i:12283;}i:9;a:2:{i:0;i:917536;i:1;i:917631;}}s:11:"replacemaps";a:1401:{i:65;a:1:{i:0;i:97;}i:66;a:1:{i:0;i:98;}i:67;a:1:{i:0;i:99;}i:68;a:1:{i:0;i:100;}i:69;a:1:{i:0;i:101;}i:70;a:1:{i:0;i:102;}i:71;a:1:{i:0;i:103;}i:72;a:1:{i:0;i:104;}i:73;a:1:{i:0;i:105;}i:74;a:1:{i:0;i:106;}i:75;a:1:{i:0;i:107;}i:76;a:1:{i:0;i:108;}i:77;a:1:{i:0;i:109;}i:78;a:1:{i:0;i:110;}i:79;a:1:{i:0;i:111;}i:80;a:1:{i:0;i:112;}i:81;a:1:{i:0;i:113;}i:82;a:1:{i:0;i:114;}i:83;a:1:{i:0;i:115;}i:84;a:1:{i:0;i:116;}i:85;a:1:{i:0;i:117;}i:86;a:1:{i:0;i:118;}i:87;a:1:{i:0;i:119;}i:88;a:1:{i:0;i:120;}i:89;a:1:{i:0;i:121;}i:90;a:1:{i:0;i:122;}i:181;a:1:{i:0;i:956;}i:192;a:1:{i:0;i:224;}i:193;a:1:{i:0;i:225;}i:194;a:1:{i:0;i:226;}i:195;a:1:{i:0;i:227;}i:196;a:1:{i:0;i:228;}i:197;a:1:{i:0;i:229;}i:198;a:1:{i:0;i:230;}i:199;a:1:{i:0;i:231;}i:200;a:1:{i:0;i:232;}i:201;a:1:{i:0;i:233;}i:202;a:1:{i:0;i:234;}i:203;a:1:{i:0;i:235;}i:204;a:1:{i:0;i:236;}i:205;a:1:{i:0;i:237;}i:206;a:1:{i:0;i:238;}i:207;a:1:{i:0;i:239;}i:208;a:1:{i:0;i:240;}i:209;a:1:{i:0;i:241;}i:210;a:1:{i:0;i:242;}i:211;a:1:{i:0;i:243;}i:212;a:1:{i:0;i:244;}i:213;a:1:{i:0;i:245;}i:214;a:1:{i:0;i:246;}i:216;a:1:{i:0;i:248;}i:217;a:1:{i:0;i:249;}i:218;a:1:{i:0;i:250;}i:219;a:1:{i:0;i:251;}i:220;a:1:{i:0;i:252;}i:221;a:1:{i:0;i:253;}i:222;a:1:{i:0;i:254;}i:223;a:2:{i:0;i:115;i:1;i:115;}i:256;a:1:{i:0;i:257;}i:258;a:1:{i:0;i:259;}i:260;a:1:{i:0;i:261;}i:262;a:1:{i:0;i:263;}i:264;a:1:{i:0;i:265;}i:266;a:1:{i:0;i:267;}i:268;a:1:{i:0;i:269;}i:270;a:1:{i:0;i:271;}i:272;a:1:{i:0;i:273;}i:274;a:1:{i:0;i:275;}i:276;a:1:{i:0;i:277;}i:278;a:1:{i:0;i:279;}i:280;a:1:{i:0;i:281;}i:282;a:1:{i:0;i:283;}i:284;a:1:{i:0;i:285;}i:286;a:1:{i:0;i:287;}i:288;a:1:{i:0;i:289;}i:290;a:1:{i:0;i:291;}i:292;a:1:{i:0;i:293;}i:294;a:1:{i:0;i:295;}i:296;a:1:{i:0;i:297;}i:298;a:1:{i:0;i:299;}i:300;a:1:{i:0;i:301;}i:302;a:1:{i:0;i:303;}i:304;a:2:{i:0;i:105;i:1;i:775;}i:306;a:1:{i:0;i:307;}i:308;a:1:{i:0;i:309;}i:310;a:1:{i:0;i:311;}i:313;a:1:{i:0;i:314;}i:315;a:1:{i:0;i:316;}i:317;a:1:{i:0;i:318;}i:319;a:1:{i:0;i:320;}i:321;a:1:{i:0;i:322;}i:323;a:1:{i:0;i:324;}i:325;a:1:{i:0;i:326;}i:327;a:1:{i:0;i:328;}i:329;a:2:{i:0;i:700;i:1;i:110;}i:330;a:1:{i:0;i:331;}i:332;a:1:{i:0;i:333;}i:334;a:1:{i:0;i:335;}i:336;a:1:{i:0;i:337;}i:338;a:1:{i:0;i:339;}i:340;a:1:{i:0;i:341;}i:342;a:1:{i:0;i:343;}i:344;a:1:{i:0;i:345;}i:346;a:1:{i:0;i:347;}i:348;a:1:{i:0;i:349;}i:350;a:1:{i:0;i:351;}i:352;a:1:{i:0;i:353;}i:354;a:1:{i:0;i:355;}i:356;a:1:{i:0;i:357;}i:358;a:1:{i:0;i:359;}i:360;a:1:{i:0;i:361;}i:362;a:1:{i:0;i:363;}i:364;a:1:{i:0;i:365;}i:366;a:1:{i:0;i:367;}i:368;a:1:{i:0;i:369;}i:370;a:1:{i:0;i:371;}i:372;a:1:{i:0;i:373;}i:374;a:1:{i:0;i:375;}i:376;a:1:{i:0;i:255;}i:377;a:1:{i:0;i:378;}i:379;a:1:{i:0;i:380;}i:381;a:1:{i:0;i:382;}i:383;a:1:{i:0;i:115;}i:385;a:1:{i:0;i:595;}i:386;a:1:{i:0;i:387;}i:388;a:1:{i:0;i:389;}i:390;a:1:{i:0;i:596;}i:391;a:1:{i:0;i:392;}i:393;a:1:{i:0;i:598;}i:394;a:1:{i:0;i:599;}i:395;a:1:{i:0;i:396;}i:398;a:1:{i:0;i:477;}i:399;a:1:{i:0;i:601;}i:400;a:1:{i:0;i:603;}i:401;a:1:{i:0;i:402;}i:403;a:1:{i:0;i:608;}i:404;a:1:{i:0;i:611;}i:406;a:1:{i:0;i:617;}i:407;a:1:{i:0;i:616;}i:408;a:1:{i:0;i:409;}i:412;a:1:{i:0;i:623;}i:413;a:1:{i:0;i:626;}i:415;a:1:{i:0;i:629;}i:416;a:1:{i:0;i:417;}i:418;a:1:{i:0;i:419;}i:420;a:1:{i:0;i:421;}i:422;a:1:{i:0;i:640;}i:423;a:1:{i:0;i:424;}i:425;a:1:{i:0;i:643;}i:428;a:1:{i:0;i:429;}i:430;a:1:{i:0;i:648;}i:431;a:1:{i:0;i:432;}i:433;a:1:{i:0;i:650;}i:434;a:1:{i:0;i:651;}i:435;a:1:{i:0;i:436;}i:437;a:1:{i:0;i:438;}i:439;a:1:{i:0;i:658;}i:440;a:1:{i:0;i:441;}i:444;a:1:{i:0;i:445;}i:452;a:1:{i:0;i:454;}i:453;a:1:{i:0;i:454;}i:455;a:1:{i:0;i:457;}i:456;a:1:{i:0;i:457;}i:458;a:1:{i:0;i:460;}i:459;a:1:{i:0;i:460;}i:461;a:1:{i:0;i:462;}i:463;a:1:{i:0;i:464;}i:465;a:1:{i:0;i:466;}i:467;a:1:{i:0;i:468;}i:469;a:1:{i:0;i:470;}i:471;a:1:{i:0;i:472;}i:473;a:1:{i:0;i:474;}i:475;a:1:{i:0;i:476;}i:478;a:1:{i:0;i:479;}i:480;a:1:{i:0;i:481;}i:482;a:1:{i:0;i:483;}i:484;a:1:{i:0;i:485;}i:486;a:1:{i:0;i:487;}i:488;a:1:{i:0;i:489;}i:490;a:1:{i:0;i:491;}i:492;a:1:{i:0;i:493;}i:494;a:1:{i:0;i:495;}i:496;a:2:{i:0;i:106;i:1;i:780;}i:497;a:1:{i:0;i:499;}i:498;a:1:{i:0;i:499;}i:500;a:1:{i:0;i:501;}i:502;a:1:{i:0;i:405;}i:503;a:1:{i:0;i:447;}i:504;a:1:{i:0;i:505;}i:506;a:1:{i:0;i:507;}i:508;a:1:{i:0;i:509;}i:510;a:1:{i:0;i:511;}i:512;a:1:{i:0;i:513;}i:514;a:1:{i:0;i:515;}i:516;a:1:{i:0;i:517;}i:518;a:1:{i:0;i:519;}i:520;a:1:{i:0;i:521;}i:522;a:1:{i:0;i:523;}i:524;a:1:{i:0;i:525;}i:526;a:1:{i:0;i:527;}i:528;a:1:{i:0;i:529;}i:530;a:1:{i:0;i:531;}i:532;a:1:{i:0;i:533;}i:534;a:1:{i:0;i:535;}i:536;a:1:{i:0;i:537;}i:538;a:1:{i:0;i:539;}i:540;a:1:{i:0;i:541;}i:542;a:1:{i:0;i:543;}i:544;a:1:{i:0;i:414;}i:546;a:1:{i:0;i:547;}i:548;a:1:{i:0;i:549;}i:550;a:1:{i:0;i:551;}i:552;a:1:{i:0;i:553;}i:554;a:1:{i:0;i:555;}i:556;a:1:{i:0;i:557;}i:558;a:1:{i:0;i:559;}i:560;a:1:{i:0;i:561;}i:562;a:1:{i:0;i:563;}i:837;a:1:{i:0;i:953;}i:890;a:2:{i:0;i:32;i:1;i:953;}i:902;a:1:{i:0;i:940;}i:904;a:1:{i:0;i:941;}i:905;a:1:{i:0;i:942;}i:906;a:1:{i:0;i:943;}i:908;a:1:{i:0;i:972;}i:910;a:1:{i:0;i:973;}i:911;a:1:{i:0;i:974;}i:912;a:3:{i:0;i:953;i:1;i:776;i:2;i:769;}i:913;a:1:{i:0;i:945;}i:914;a:1:{i:0;i:946;}i:915;a:1:{i:0;i:947;}i:916;a:1:{i:0;i:948;}i:917;a:1:{i:0;i:949;}i:918;a:1:{i:0;i:950;}i:919;a:1:{i:0;i:951;}i:920;a:1:{i:0;i:952;}i:921;a:1:{i:0;i:953;}i:922;a:1:{i:0;i:954;}i:923;a:1:{i:0;i:955;}i:924;a:1:{i:0;i:956;}i:925;a:1:{i:0;i:957;}i:926;a:1:{i:0;i:958;}i:927;a:1:{i:0;i:959;}i:928;a:1:{i:0;i:960;}i:929;a:1:{i:0;i:961;}i:931;a:1:{i:0;i:963;}i:932;a:1:{i:0;i:964;}i:933;a:1:{i:0;i:965;}i:934;a:1:{i:0;i:966;}i:935;a:1:{i:0;i:967;}i:936;a:1:{i:0;i:968;}i:937;a:1:{i:0;i:969;}i:938;a:1:{i:0;i:970;}i:939;a:1:{i:0;i:971;}i:944;a:3:{i:0;i:965;i:1;i:776;i:2;i:769;}i:962;a:1:{i:0;i:963;}i:976;a:1:{i:0;i:946;}i:977;a:1:{i:0;i:952;}i:978;a:1:{i:0;i:965;}i:979;a:1:{i:0;i:973;}i:980;a:1:{i:0;i:971;}i:981;a:1:{i:0;i:966;}i:982;a:1:{i:0;i:960;}i:984;a:1:{i:0;i:985;}i:986;a:1:{i:0;i:987;}i:988;a:1:{i:0;i:989;}i:990;a:1:{i:0;i:991;}i:992;a:1:{i:0;i:993;}i:994;a:1:{i:0;i:995;}i:996;a:1:{i:0;i:997;}i:998;a:1:{i:0;i:999;}i:1000;a:1:{i:0;i:1001;}i:1002;a:1:{i:0;i:1003;}i:1004;a:1:{i:0;i:1005;}i:1006;a:1:{i:0;i:1007;}i:1008;a:1:{i:0;i:954;}i:1009;a:1:{i:0;i:961;}i:1010;a:1:{i:0;i:963;}i:1012;a:1:{i:0;i:952;}i:1013;a:1:{i:0;i:949;}i:1024;a:1:{i:0;i:1104;}i:1025;a:1:{i:0;i:1105;}i:1026;a:1:{i:0;i:1106;}i:1027;a:1:{i:0;i:1107;}i:1028;a:1:{i:0;i:1108;}i:1029;a:1:{i:0;i:1109;}i:1030;a:1:{i:0;i:1110;}i:1031;a:1:{i:0;i:1111;}i:1032;a:1:{i:0;i:1112;}i:1033;a:1:{i:0;i:1113;}i:1034;a:1:{i:0;i:1114;}i:1035;a:1:{i:0;i:1115;}i:1036;a:1:{i:0;i:1116;}i:1037;a:1:{i:0;i:1117;}i:1038;a:1:{i:0;i:1118;}i:1039;a:1:{i:0;i:1119;}i:1040;a:1:{i:0;i:1072;}i:1041;a:1:{i:0;i:1073;}i:1042;a:1:{i:0;i:1074;}i:1043;a:1:{i:0;i:1075;}i:1044;a:1:{i:0;i:1076;}i:1045;a:1:{i:0;i:1077;}i:1046;a:1:{i:0;i:1078;}i:1047;a:1:{i:0;i:1079;}i:1048;a:1:{i:0;i:1080;}i:1049;a:1:{i:0;i:1081;}i:1050;a:1:{i:0;i:1082;}i:1051;a:1:{i:0;i:1083;}i:1052;a:1:{i:0;i:1084;}i:1053;a:1:{i:0;i:1085;}i:1054;a:1:{i:0;i:1086;}i:1055;a:1:{i:0;i:1087;}i:1056;a:1:{i:0;i:1088;}i:1057;a:1:{i:0;i:1089;}i:1058;a:1:{i:0;i:1090;}i:1059;a:1:{i:0;i:1091;}i:1060;a:1:{i:0;i:1092;}i:1061;a:1:{i:0;i:1093;}i:1062;a:1:{i:0;i:1094;}i:1063;a:1:{i:0;i:1095;}i:1064;a:1:{i:0;i:1096;}i:1065;a:1:{i:0;i:1097;}i:1066;a:1:{i:0;i:1098;}i:1067;a:1:{i:0;i:1099;}i:1068;a:1:{i:0;i:1100;}i:1069;a:1:{i:0;i:1101;}i:1070;a:1:{i:0;i:1102;}i:1071;a:1:{i:0;i:1103;}i:1120;a:1:{i:0;i:1121;}i:1122;a:1:{i:0;i:1123;}i:1124;a:1:{i:0;i:1125;}i:1126;a:1:{i:0;i:1127;}i:1128;a:1:{i:0;i:1129;}i:1130;a:1:{i:0;i:1131;}i:1132;a:1:{i:0;i:1133;}i:1134;a:1:{i:0;i:1135;}i:1136;a:1:{i:0;i:1137;}i:1138;a:1:{i:0;i:1139;}i:1140;a:1:{i:0;i:1141;}i:1142;a:1:{i:0;i:1143;}i:1144;a:1:{i:0;i:1145;}i:1146;a:1:{i:0;i:1147;}i:1148;a:1:{i:0;i:1149;}i:1150;a:1:{i:0;i:1151;}i:1152;a:1:{i:0;i:1153;}i:1162;a:1:{i:0;i:1163;}i:1164;a:1:{i:0;i:1165;}i:1166;a:1:{i:0;i:1167;}i:1168;a:1:{i:0;i:1169;}i:1170;a:1:{i:0;i:1171;}i:1172;a:1:{i:0;i:1173;}i:1174;a:1:{i:0;i:1175;}i:1176;a:1:{i:0;i:1177;}i:1178;a:1:{i:0;i:1179;}i:1180;a:1:{i:0;i:1181;}i:1182;a:1:{i:0;i:1183;}i:1184;a:1:{i:0;i:1185;}i:1186;a:1:{i:0;i:1187;}i:1188;a:1:{i:0;i:1189;}i:1190;a:1:{i:0;i:1191;}i:1192;a:1:{i:0;i:1193;}i:1194;a:1:{i:0;i:1195;}i:1196;a:1:{i:0;i:1197;}i:1198;a:1:{i:0;i:1199;}i:1200;a:1:{i:0;i:1201;}i:1202;a:1:{i:0;i:1203;}i:1204;a:1:{i:0;i:1205;}i:1206;a:1:{i:0;i:1207;}i:1208;a:1:{i:0;i:1209;}i:1210;a:1:{i:0;i:1211;}i:1212;a:1:{i:0;i:1213;}i:1214;a:1:{i:0;i:1215;}i:1217;a:1:{i:0;i:1218;}i:1219;a:1:{i:0;i:1220;}i:1221;a:1:{i:0;i:1222;}i:1223;a:1:{i:0;i:1224;}i:1225;a:1:{i:0;i:1226;}i:1227;a:1:{i:0;i:1228;}i:1229;a:1:{i:0;i:1230;}i:1232;a:1:{i:0;i:1233;}i:1234;a:1:{i:0;i:1235;}i:1236;a:1:{i:0;i:1237;}i:1238;a:1:{i:0;i:1239;}i:1240;a:1:{i:0;i:1241;}i:1242;a:1:{i:0;i:1243;}i:1244;a:1:{i:0;i:1245;}i:1246;a:1:{i:0;i:1247;}i:1248;a:1:{i:0;i:1249;}i:1250;a:1:{i:0;i:1251;}i:1252;a:1:{i:0;i:1253;}i:1254;a:1:{i:0;i:1255;}i:1256;a:1:{i:0;i:1257;}i:1258;a:1:{i:0;i:1259;}i:1260;a:1:{i:0;i:1261;}i:1262;a:1:{i:0;i:1263;}i:1264;a:1:{i:0;i:1265;}i:1266;a:1:{i:0;i:1267;}i:1268;a:1:{i:0;i:1269;}i:1272;a:1:{i:0;i:1273;}i:1280;a:1:{i:0;i:1281;}i:1282;a:1:{i:0;i:1283;}i:1284;a:1:{i:0;i:1285;}i:1286;a:1:{i:0;i:1287;}i:1288;a:1:{i:0;i:1289;}i:1290;a:1:{i:0;i:1291;}i:1292;a:1:{i:0;i:1293;}i:1294;a:1:{i:0;i:1295;}i:1329;a:1:{i:0;i:1377;}i:1330;a:1:{i:0;i:1378;}i:1331;a:1:{i:0;i:1379;}i:1332;a:1:{i:0;i:1380;}i:1333;a:1:{i:0;i:1381;}i:1334;a:1:{i:0;i:1382;}i:1335;a:1:{i:0;i:1383;}i:1336;a:1:{i:0;i:1384;}i:1337;a:1:{i:0;i:1385;}i:1338;a:1:{i:0;i:1386;}i:1339;a:1:{i:0;i:1387;}i:1340;a:1:{i:0;i:1388;}i:1341;a:1:{i:0;i:1389;}i:1342;a:1:{i:0;i:1390;}i:1343;a:1:{i:0;i:1391;}i:1344;a:1:{i:0;i:1392;}i:1345;a:1:{i:0;i:1393;}i:1346;a:1:{i:0;i:1394;}i:1347;a:1:{i:0;i:1395;}i:1348;a:1:{i:0;i:1396;}i:1349;a:1:{i:0;i:1397;}i:1350;a:1:{i:0;i:1398;}i:1351;a:1:{i:0;i:1399;}i:1352;a:1:{i:0;i:1400;}i:1353;a:1:{i:0;i:1401;}i:1354;a:1:{i:0;i:1402;}i:1355;a:1:{i:0;i:1403;}i:1356;a:1:{i:0;i:1404;}i:1357;a:1:{i:0;i:1405;}i:1358;a:1:{i:0;i:1406;}i:1359;a:1:{i:0;i:1407;}i:1360;a:1:{i:0;i:1408;}i:1361;a:1:{i:0;i:1409;}i:1362;a:1:{i:0;i:1410;}i:1363;a:1:{i:0;i:1411;}i:1364;a:1:{i:0;i:1412;}i:1365;a:1:{i:0;i:1413;}i:1366;a:1:{i:0;i:1414;}i:1415;a:2:{i:0;i:1381;i:1;i:1410;}i:7680;a:1:{i:0;i:7681;}i:7682;a:1:{i:0;i:7683;}i:7684;a:1:{i:0;i:7685;}i:7686;a:1:{i:0;i:7687;}i:7688;a:1:{i:0;i:7689;}i:7690;a:1:{i:0;i:7691;}i:7692;a:1:{i:0;i:7693;}i:7694;a:1:{i:0;i:7695;}i:7696;a:1:{i:0;i:7697;}i:7698;a:1:{i:0;i:7699;}i:7700;a:1:{i:0;i:7701;}i:7702;a:1:{i:0;i:7703;}i:7704;a:1:{i:0;i:7705;}i:7706;a:1:{i:0;i:7707;}i:7708;a:1:{i:0;i:7709;}i:7710;a:1:{i:0;i:7711;}i:7712;a:1:{i:0;i:7713;}i:7714;a:1:{i:0;i:7715;}i:7716;a:1:{i:0;i:7717;}i:7718;a:1:{i:0;i:7719;}i:7720;a:1:{i:0;i:7721;}i:7722;a:1:{i:0;i:7723;}i:7724;a:1:{i:0;i:7725;}i:7726;a:1:{i:0;i:7727;}i:7728;a:1:{i:0;i:7729;}i:7730;a:1:{i:0;i:7731;}i:7732;a:1:{i:0;i:7733;}i:7734;a:1:{i:0;i:7735;}i:7736;a:1:{i:0;i:7737;}i:7738;a:1:{i:0;i:7739;}i:7740;a:1:{i:0;i:7741;}i:7742;a:1:{i:0;i:7743;}i:7744;a:1:{i:0;i:7745;}i:7746;a:1:{i:0;i:7747;}i:7748;a:1:{i:0;i:7749;}i:7750;a:1:{i:0;i:7751;}i:7752;a:1:{i:0;i:7753;}i:7754;a:1:{i:0;i:7755;}i:7756;a:1:{i:0;i:7757;}i:7758;a:1:{i:0;i:7759;}i:7760;a:1:{i:0;i:7761;}i:7762;a:1:{i:0;i:7763;}i:7764;a:1:{i:0;i:7765;}i:7766;a:1:{i:0;i:7767;}i:7768;a:1:{i:0;i:7769;}i:7770;a:1:{i:0;i:7771;}i:7772;a:1:{i:0;i:7773;}i:7774;a:1:{i:0;i:7775;}i:7776;a:1:{i:0;i:7777;}i:7778;a:1:{i:0;i:7779;}i:7780;a:1:{i:0;i:7781;}i:7782;a:1:{i:0;i:7783;}i:7784;a:1:{i:0;i:7785;}i:7786;a:1:{i:0;i:7787;}i:7788;a:1:{i:0;i:7789;}i:7790;a:1:{i:0;i:7791;}i:7792;a:1:{i:0;i:7793;}i:7794;a:1:{i:0;i:7795;}i:7796;a:1:{i:0;i:7797;}i:7798;a:1:{i:0;i:7799;}i:7800;a:1:{i:0;i:7801;}i:7802;a:1:{i:0;i:7803;}i:7804;a:1:{i:0;i:7805;}i:7806;a:1:{i:0;i:7807;}i:7808;a:1:{i:0;i:7809;}i:7810;a:1:{i:0;i:7811;}i:7812;a:1:{i:0;i:7813;}i:7814;a:1:{i:0;i:7815;}i:7816;a:1:{i:0;i:7817;}i:7818;a:1:{i:0;i:7819;}i:7820;a:1:{i:0;i:7821;}i:7822;a:1:{i:0;i:7823;}i:7824;a:1:{i:0;i:7825;}i:7826;a:1:{i:0;i:7827;}i:7828;a:1:{i:0;i:7829;}i:7830;a:2:{i:0;i:104;i:1;i:817;}i:7831;a:2:{i:0;i:116;i:1;i:776;}i:7832;a:2:{i:0;i:119;i:1;i:778;}i:7833;a:2:{i:0;i:121;i:1;i:778;}i:7834;a:2:{i:0;i:97;i:1;i:702;}i:7835;a:1:{i:0;i:7777;}i:7840;a:1:{i:0;i:7841;}i:7842;a:1:{i:0;i:7843;}i:7844;a:1:{i:0;i:7845;}i:7846;a:1:{i:0;i:7847;}i:7848;a:1:{i:0;i:7849;}i:7850;a:1:{i:0;i:7851;}i:7852;a:1:{i:0;i:7853;}i:7854;a:1:{i:0;i:7855;}i:7856;a:1:{i:0;i:7857;}i:7858;a:1:{i:0;i:7859;}i:7860;a:1:{i:0;i:7861;}i:7862;a:1:{i:0;i:7863;}i:7864;a:1:{i:0;i:7865;}i:7866;a:1:{i:0;i:7867;}i:7868;a:1:{i:0;i:7869;}i:7870;a:1:{i:0;i:7871;}i:7872;a:1:{i:0;i:7873;}i:7874;a:1:{i:0;i:7875;}i:7876;a:1:{i:0;i:7877;}i:7878;a:1:{i:0;i:7879;}i:7880;a:1:{i:0;i:7881;}i:7882;a:1:{i:0;i:7883;}i:7884;a:1:{i:0;i:7885;}i:7886;a:1:{i:0;i:7887;}i:7888;a:1:{i:0;i:7889;}i:7890;a:1:{i:0;i:7891;}i:7892;a:1:{i:0;i:7893;}i:7894;a:1:{i:0;i:7895;}i:7896;a:1:{i:0;i:7897;}i:7898;a:1:{i:0;i:7899;}i:7900;a:1:{i:0;i:7901;}i:7902;a:1:{i:0;i:7903;}i:7904;a:1:{i:0;i:7905;}i:7906;a:1:{i:0;i:7907;}i:7908;a:1:{i:0;i:7909;}i:7910;a:1:{i:0;i:7911;}i:7912;a:1:{i:0;i:7913;}i:7914;a:1:{i:0;i:7915;}i:7916;a:1:{i:0;i:7917;}i:7918;a:1:{i:0;i:7919;}i:7920;a:1:{i:0;i:7921;}i:7922;a:1:{i:0;i:7923;}i:7924;a:1:{i:0;i:7925;}i:7926;a:1:{i:0;i:7927;}i:7928;a:1:{i:0;i:7929;}i:7944;a:1:{i:0;i:7936;}i:7945;a:1:{i:0;i:7937;}i:7946;a:1:{i:0;i:7938;}i:7947;a:1:{i:0;i:7939;}i:7948;a:1:{i:0;i:7940;}i:7949;a:1:{i:0;i:7941;}i:7950;a:1:{i:0;i:7942;}i:7951;a:1:{i:0;i:7943;}i:7960;a:1:{i:0;i:7952;}i:7961;a:1:{i:0;i:7953;}i:7962;a:1:{i:0;i:7954;}i:7963;a:1:{i:0;i:7955;}i:7964;a:1:{i:0;i:7956;}i:7965;a:1:{i:0;i:7957;}i:7976;a:1:{i:0;i:7968;}i:7977;a:1:{i:0;i:7969;}i:7978;a:1:{i:0;i:7970;}i:7979;a:1:{i:0;i:7971;}i:7980;a:1:{i:0;i:7972;}i:7981;a:1:{i:0;i:7973;}i:7982;a:1:{i:0;i:7974;}i:7983;a:1:{i:0;i:7975;}i:7992;a:1:{i:0;i:7984;}i:7993;a:1:{i:0;i:7985;}i:7994;a:1:{i:0;i:7986;}i:7995;a:1:{i:0;i:7987;}i:7996;a:1:{i:0;i:7988;}i:7997;a:1:{i:0;i:7989;}i:7998;a:1:{i:0;i:7990;}i:7999;a:1:{i:0;i:7991;}i:8008;a:1:{i:0;i:8000;}i:8009;a:1:{i:0;i:8001;}i:8010;a:1:{i:0;i:8002;}i:8011;a:1:{i:0;i:8003;}i:8012;a:1:{i:0;i:8004;}i:8013;a:1:{i:0;i:8005;}i:8016;a:2:{i:0;i:965;i:1;i:787;}i:8018;a:3:{i:0;i:965;i:1;i:787;i:2;i:768;}i:8020;a:3:{i:0;i:965;i:1;i:787;i:2;i:769;}i:8022;a:3:{i:0;i:965;i:1;i:787;i:2;i:834;}i:8025;a:1:{i:0;i:8017;}i:8027;a:1:{i:0;i:8019;}i:8029;a:1:{i:0;i:8021;}i:8031;a:1:{i:0;i:8023;}i:8040;a:1:{i:0;i:8032;}i:8041;a:1:{i:0;i:8033;}i:8042;a:1:{i:0;i:8034;}i:8043;a:1:{i:0;i:8035;}i:8044;a:1:{i:0;i:8036;}i:8045;a:1:{i:0;i:8037;}i:8046;a:1:{i:0;i:8038;}i:8047;a:1:{i:0;i:8039;}i:8064;a:2:{i:0;i:7936;i:1;i:953;}i:8065;a:2:{i:0;i:7937;i:1;i:953;}i:8066;a:2:{i:0;i:7938;i:1;i:953;}i:8067;a:2:{i:0;i:7939;i:1;i:953;}i:8068;a:2:{i:0;i:7940;i:1;i:953;}i:8069;a:2:{i:0;i:7941;i:1;i:953;}i:8070;a:2:{i:0;i:7942;i:1;i:953;}i:8071;a:2:{i:0;i:7943;i:1;i:953;}i:8072;a:2:{i:0;i:7936;i:1;i:953;}i:8073;a:2:{i:0;i:7937;i:1;i:953;}i:8074;a:2:{i:0;i:7938;i:1;i:953;}i:8075;a:2:{i:0;i:7939;i:1;i:953;}i:8076;a:2:{i:0;i:7940;i:1;i:953;}i:8077;a:2:{i:0;i:7941;i:1;i:953;}i:8078;a:2:{i:0;i:7942;i:1;i:953;}i:8079;a:2:{i:0;i:7943;i:1;i:953;}i:8080;a:2:{i:0;i:7968;i:1;i:953;}i:8081;a:2:{i:0;i:7969;i:1;i:953;}i:8082;a:2:{i:0;i:7970;i:1;i:953;}i:8083;a:2:{i:0;i:7971;i:1;i:953;}i:8084;a:2:{i:0;i:7972;i:1;i:953;}i:8085;a:2:{i:0;i:7973;i:1;i:953;}i:8086;a:2:{i:0;i:7974;i:1;i:953;}i:8087;a:2:{i:0;i:7975;i:1;i:953;}i:8088;a:2:{i:0;i:7968;i:1;i:953;}i:8089;a:2:{i:0;i:7969;i:1;i:953;}i:8090;a:2:{i:0;i:7970;i:1;i:953;}i:8091;a:2:{i:0;i:7971;i:1;i:953;}i:8092;a:2:{i:0;i:7972;i:1;i:953;}i:8093;a:2:{i:0;i:7973;i:1;i:953;}i:8094;a:2:{i:0;i:7974;i:1;i:953;}i:8095;a:2:{i:0;i:7975;i:1;i:953;}i:8096;a:2:{i:0;i:8032;i:1;i:953;}i:8097;a:2:{i:0;i:8033;i:1;i:953;}i:8098;a:2:{i:0;i:8034;i:1;i:953;}i:8099;a:2:{i:0;i:8035;i:1;i:953;}i:8100;a:2:{i:0;i:8036;i:1;i:953;}i:8101;a:2:{i:0;i:8037;i:1;i:953;}i:8102;a:2:{i:0;i:8038;i:1;i:953;}i:8103;a:2:{i:0;i:8039;i:1;i:953;}i:8104;a:2:{i:0;i:8032;i:1;i:953;}i:8105;a:2:{i:0;i:8033;i:1;i:953;}i:8106;a:2:{i:0;i:8034;i:1;i:953;}i:8107;a:2:{i:0;i:8035;i:1;i:953;}i:8108;a:2:{i:0;i:8036;i:1;i:953;}i:8109;a:2:{i:0;i:8037;i:1;i:953;}i:8110;a:2:{i:0;i:8038;i:1;i:953;}i:8111;a:2:{i:0;i:8039;i:1;i:953;}i:8114;a:2:{i:0;i:8048;i:1;i:953;}i:8115;a:2:{i:0;i:945;i:1;i:953;}i:8116;a:2:{i:0;i:940;i:1;i:953;}i:8118;a:2:{i:0;i:945;i:1;i:834;}i:8119;a:3:{i:0;i:945;i:1;i:834;i:2;i:953;}i:8120;a:1:{i:0;i:8112;}i:8121;a:1:{i:0;i:8113;}i:8122;a:1:{i:0;i:8048;}i:8123;a:1:{i:0;i:8049;}i:8124;a:2:{i:0;i:945;i:1;i:953;}i:8126;a:1:{i:0;i:953;}i:8130;a:2:{i:0;i:8052;i:1;i:953;}i:8131;a:2:{i:0;i:951;i:1;i:953;}i:8132;a:2:{i:0;i:942;i:1;i:953;}i:8134;a:2:{i:0;i:951;i:1;i:834;}i:8135;a:3:{i:0;i:951;i:1;i:834;i:2;i:953;}i:8136;a:1:{i:0;i:8050;}i:8137;a:1:{i:0;i:8051;}i:8138;a:1:{i:0;i:8052;}i:8139;a:1:{i:0;i:8053;}i:8140;a:2:{i:0;i:951;i:1;i:953;}i:8146;a:3:{i:0;i:953;i:1;i:776;i:2;i:768;}i:8147;a:3:{i:0;i:953;i:1;i:776;i:2;i:769;}i:8150;a:2:{i:0;i:953;i:1;i:834;}i:8151;a:3:{i:0;i:953;i:1;i:776;i:2;i:834;}i:8152;a:1:{i:0;i:8144;}i:8153;a:1:{i:0;i:8145;}i:8154;a:1:{i:0;i:8054;}i:8155;a:1:{i:0;i:8055;}i:8162;a:3:{i:0;i:965;i:1;i:776;i:2;i:768;}i:8163;a:3:{i:0;i:965;i:1;i:776;i:2;i:769;}i:8164;a:2:{i:0;i:961;i:1;i:787;}i:8166;a:2:{i:0;i:965;i:1;i:834;}i:8167;a:3:{i:0;i:965;i:1;i:776;i:2;i:834;}i:8168;a:1:{i:0;i:8160;}i:8169;a:1:{i:0;i:8161;}i:8170;a:1:{i:0;i:8058;}i:8171;a:1:{i:0;i:8059;}i:8172;a:1:{i:0;i:8165;}i:8178;a:2:{i:0;i:8060;i:1;i:953;}i:8179;a:2:{i:0;i:969;i:1;i:953;}i:8180;a:2:{i:0;i:974;i:1;i:953;}i:8182;a:2:{i:0;i:969;i:1;i:834;}i:8183;a:3:{i:0;i:969;i:1;i:834;i:2;i:953;}i:8184;a:1:{i:0;i:8056;}i:8185;a:1:{i:0;i:8057;}i:8186;a:1:{i:0;i:8060;}i:8187;a:1:{i:0;i:8061;}i:8188;a:2:{i:0;i:969;i:1;i:953;}i:8360;a:2:{i:0;i:114;i:1;i:115;}i:8450;a:1:{i:0;i:99;}i:8451;a:2:{i:0;i:176;i:1;i:99;}i:8455;a:1:{i:0;i:603;}i:8457;a:2:{i:0;i:176;i:1;i:102;}i:8459;a:1:{i:0;i:104;}i:8460;a:1:{i:0;i:104;}i:8461;a:1:{i:0;i:104;}i:8464;a:1:{i:0;i:105;}i:8465;a:1:{i:0;i:105;}i:8466;a:1:{i:0;i:108;}i:8469;a:1:{i:0;i:110;}i:8470;a:2:{i:0;i:110;i:1;i:111;}i:8473;a:1:{i:0;i:112;}i:8474;a:1:{i:0;i:113;}i:8475;a:1:{i:0;i:114;}i:8476;a:1:{i:0;i:114;}i:8477;a:1:{i:0;i:114;}i:8480;a:2:{i:0;i:115;i:1;i:109;}i:8481;a:3:{i:0;i:116;i:1;i:101;i:2;i:108;}i:8482;a:2:{i:0;i:116;i:1;i:109;}i:8484;a:1:{i:0;i:122;}i:8486;a:1:{i:0;i:969;}i:8488;a:1:{i:0;i:122;}i:8490;a:1:{i:0;i:107;}i:8491;a:1:{i:0;i:229;}i:8492;a:1:{i:0;i:98;}i:8493;a:1:{i:0;i:99;}i:8496;a:1:{i:0;i:101;}i:8497;a:1:{i:0;i:102;}i:8499;a:1:{i:0;i:109;}i:8510;a:1:{i:0;i:947;}i:8511;a:1:{i:0;i:960;}i:8517;a:1:{i:0;i:100;}i:8544;a:1:{i:0;i:8560;}i:8545;a:1:{i:0;i:8561;}i:8546;a:1:{i:0;i:8562;}i:8547;a:1:{i:0;i:8563;}i:8548;a:1:{i:0;i:8564;}i:8549;a:1:{i:0;i:8565;}i:8550;a:1:{i:0;i:8566;}i:8551;a:1:{i:0;i:8567;}i:8552;a:1:{i:0;i:8568;}i:8553;a:1:{i:0;i:8569;}i:8554;a:1:{i:0;i:8570;}i:8555;a:1:{i:0;i:8571;}i:8556;a:1:{i:0;i:8572;}i:8557;a:1:{i:0;i:8573;}i:8558;a:1:{i:0;i:8574;}i:8559;a:1:{i:0;i:8575;}i:9398;a:1:{i:0;i:9424;}i:9399;a:1:{i:0;i:9425;}i:9400;a:1:{i:0;i:9426;}i:9401;a:1:{i:0;i:9427;}i:9402;a:1:{i:0;i:9428;}i:9403;a:1:{i:0;i:9429;}i:9404;a:1:{i:0;i:9430;}i:9405;a:1:{i:0;i:9431;}i:9406;a:1:{i:0;i:9432;}i:9407;a:1:{i:0;i:9433;}i:9408;a:1:{i:0;i:9434;}i:9409;a:1:{i:0;i:9435;}i:9410;a:1:{i:0;i:9436;}i:9411;a:1:{i:0;i:9437;}i:9412;a:1:{i:0;i:9438;}i:9413;a:1:{i:0;i:9439;}i:9414;a:1:{i:0;i:9440;}i:9415;a:1:{i:0;i:9441;}i:9416;a:1:{i:0;i:9442;}i:9417;a:1:{i:0;i:9443;}i:9418;a:1:{i:0;i:9444;}i:9419;a:1:{i:0;i:9445;}i:9420;a:1:{i:0;i:9446;}i:9421;a:1:{i:0;i:9447;}i:9422;a:1:{i:0;i:9448;}i:9423;a:1:{i:0;i:9449;}i:13169;a:3:{i:0;i:104;i:1;i:112;i:2;i:97;}i:13171;a:2:{i:0;i:97;i:1;i:117;}i:13173;a:2:{i:0;i:111;i:1;i:118;}i:13184;a:2:{i:0;i:112;i:1;i:97;}i:13185;a:2:{i:0;i:110;i:1;i:97;}i:13186;a:2:{i:0;i:956;i:1;i:97;}i:13187;a:2:{i:0;i:109;i:1;i:97;}i:13188;a:2:{i:0;i:107;i:1;i:97;}i:13189;a:2:{i:0;i:107;i:1;i:98;}i:13190;a:2:{i:0;i:109;i:1;i:98;}i:13191;a:2:{i:0;i:103;i:1;i:98;}i:13194;a:2:{i:0;i:112;i:1;i:102;}i:13195;a:2:{i:0;i:110;i:1;i:102;}i:13196;a:2:{i:0;i:956;i:1;i:102;}i:13200;a:2:{i:0;i:104;i:1;i:122;}i:13201;a:3:{i:0;i:107;i:1;i:104;i:2;i:122;}i:13202;a:3:{i:0;i:109;i:1;i:104;i:2;i:122;}i:13203;a:3:{i:0;i:103;i:1;i:104;i:2;i:122;}i:13204;a:3:{i:0;i:116;i:1;i:104;i:2;i:122;}i:13225;a:2:{i:0;i:112;i:1;i:97;}i:13226;a:3:{i:0;i:107;i:1;i:112;i:2;i:97;}i:13227;a:3:{i:0;i:109;i:1;i:112;i:2;i:97;}i:13228;a:3:{i:0;i:103;i:1;i:112;i:2;i:97;}i:13236;a:2:{i:0;i:112;i:1;i:118;}i:13237;a:2:{i:0;i:110;i:1;i:118;}i:13238;a:2:{i:0;i:956;i:1;i:118;}i:13239;a:2:{i:0;i:109;i:1;i:118;}i:13240;a:2:{i:0;i:107;i:1;i:118;}i:13241;a:2:{i:0;i:109;i:1;i:118;}i:13242;a:2:{i:0;i:112;i:1;i:119;}i:13243;a:2:{i:0;i:110;i:1;i:119;}i:13244;a:2:{i:0;i:956;i:1;i:119;}i:13245;a:2:{i:0;i:109;i:1;i:119;}i:13246;a:2:{i:0;i:107;i:1;i:119;}i:13247;a:2:{i:0;i:109;i:1;i:119;}i:13248;a:2:{i:0;i:107;i:1;i:969;}i:13249;a:2:{i:0;i:109;i:1;i:969;}i:13251;a:2:{i:0;i:98;i:1;i:113;}i:13254;a:4:{i:0;i:99;i:1;i:8725;i:2;i:107;i:3;i:103;}i:13255;a:3:{i:0;i:99;i:1;i:111;i:2;i:46;}i:13256;a:2:{i:0;i:100;i:1;i:98;}i:13257;a:2:{i:0;i:103;i:1;i:121;}i:13259;a:2:{i:0;i:104;i:1;i:112;}i:13261;a:2:{i:0;i:107;i:1;i:107;}i:13262;a:2:{i:0;i:107;i:1;i:109;}i:13271;a:2:{i:0;i:112;i:1;i:104;}i:13273;a:3:{i:0;i:112;i:1;i:112;i:2;i:109;}i:13274;a:2:{i:0;i:112;i:1;i:114;}i:13276;a:2:{i:0;i:115;i:1;i:118;}i:13277;a:2:{i:0;i:119;i:1;i:98;}i:64256;a:2:{i:0;i:102;i:1;i:102;}i:64257;a:2:{i:0;i:102;i:1;i:105;}i:64258;a:2:{i:0;i:102;i:1;i:108;}i:64259;a:3:{i:0;i:102;i:1;i:102;i:2;i:105;}i:64260;a:3:{i:0;i:102;i:1;i:102;i:2;i:108;}i:64261;a:2:{i:0;i:115;i:1;i:116;}i:64262;a:2:{i:0;i:115;i:1;i:116;}i:64275;a:2:{i:0;i:1396;i:1;i:1398;}i:64276;a:2:{i:0;i:1396;i:1;i:1381;}i:64277;a:2:{i:0;i:1396;i:1;i:1387;}i:64278;a:2:{i:0;i:1406;i:1;i:1398;}i:64279;a:2:{i:0;i:1396;i:1;i:1389;}i:65313;a:1:{i:0;i:65345;}i:65314;a:1:{i:0;i:65346;}i:65315;a:1:{i:0;i:65347;}i:65316;a:1:{i:0;i:65348;}i:65317;a:1:{i:0;i:65349;}i:65318;a:1:{i:0;i:65350;}i:65319;a:1:{i:0;i:65351;}i:65320;a:1:{i:0;i:65352;}i:65321;a:1:{i:0;i:65353;}i:65322;a:1:{i:0;i:65354;}i:65323;a:1:{i:0;i:65355;}i:65324;a:1:{i:0;i:65356;}i:65325;a:1:{i:0;i:65357;}i:65326;a:1:{i:0;i:65358;}i:65327;a:1:{i:0;i:65359;}i:65328;a:1:{i:0;i:65360;}i:65329;a:1:{i:0;i:65361;}i:65330;a:1:{i:0;i:65362;}i:65331;a:1:{i:0;i:65363;}i:65332;a:1:{i:0;i:65364;}i:65333;a:1:{i:0;i:65365;}i:65334;a:1:{i:0;i:65366;}i:65335;a:1:{i:0;i:65367;}i:65336;a:1:{i:0;i:65368;}i:65337;a:1:{i:0;i:65369;}i:65338;a:1:{i:0;i:65370;}i:66560;a:1:{i:0;i:66600;}i:66561;a:1:{i:0;i:66601;}i:66562;a:1:{i:0;i:66602;}i:66563;a:1:{i:0;i:66603;}i:66564;a:1:{i:0;i:66604;}i:66565;a:1:{i:0;i:66605;}i:66566;a:1:{i:0;i:66606;}i:66567;a:1:{i:0;i:66607;}i:66568;a:1:{i:0;i:66608;}i:66569;a:1:{i:0;i:66609;}i:66570;a:1:{i:0;i:66610;}i:66571;a:1:{i:0;i:66611;}i:66572;a:1:{i:0;i:66612;}i:66573;a:1:{i:0;i:66613;}i:66574;a:1:{i:0;i:66614;}i:66575;a:1:{i:0;i:66615;}i:66576;a:1:{i:0;i:66616;}i:66577;a:1:{i:0;i:66617;}i:66578;a:1:{i:0;i:66618;}i:66579;a:1:{i:0;i:66619;}i:66580;a:1:{i:0;i:66620;}i:66581;a:1:{i:0;i:66621;}i:66582;a:1:{i:0;i:66622;}i:66583;a:1:{i:0;i:66623;}i:66584;a:1:{i:0;i:66624;}i:66585;a:1:{i:0;i:66625;}i:66586;a:1:{i:0;i:66626;}i:66587;a:1:{i:0;i:66627;}i:66588;a:1:{i:0;i:66628;}i:66589;a:1:{i:0;i:66629;}i:66590;a:1:{i:0;i:66630;}i:66591;a:1:{i:0;i:66631;}i:66592;a:1:{i:0;i:66632;}i:66593;a:1:{i:0;i:66633;}i:66594;a:1:{i:0;i:66634;}i:66595;a:1:{i:0;i:66635;}i:66596;a:1:{i:0;i:66636;}i:66597;a:1:{i:0;i:66637;}i:119808;a:1:{i:0;i:97;}i:119809;a:1:{i:0;i:98;}i:119810;a:1:{i:0;i:99;}i:119811;a:1:{i:0;i:100;}i:119812;a:1:{i:0;i:101;}i:119813;a:1:{i:0;i:102;}i:119814;a:1:{i:0;i:103;}i:119815;a:1:{i:0;i:104;}i:119816;a:1:{i:0;i:105;}i:119817;a:1:{i:0;i:106;}i:119818;a:1:{i:0;i:107;}i:119819;a:1:{i:0;i:108;}i:119820;a:1:{i:0;i:109;}i:119821;a:1:{i:0;i:110;}i:119822;a:1:{i:0;i:111;}i:119823;a:1:{i:0;i:112;}i:119824;a:1:{i:0;i:113;}i:119825;a:1:{i:0;i:114;}i:119826;a:1:{i:0;i:115;}i:119827;a:1:{i:0;i:116;}i:119828;a:1:{i:0;i:117;}i:119829;a:1:{i:0;i:118;}i:119830;a:1:{i:0;i:119;}i:119831;a:1:{i:0;i:120;}i:119832;a:1:{i:0;i:121;}i:119833;a:1:{i:0;i:122;}i:119860;a:1:{i:0;i:97;}i:119861;a:1:{i:0;i:98;}i:119862;a:1:{i:0;i:99;}i:119863;a:1:{i:0;i:100;}i:119864;a:1:{i:0;i:101;}i:119865;a:1:{i:0;i:102;}i:119866;a:1:{i:0;i:103;}i:119867;a:1:{i:0;i:104;}i:119868;a:1:{i:0;i:105;}i:119869;a:1:{i:0;i:106;}i:119870;a:1:{i:0;i:107;}i:119871;a:1:{i:0;i:108;}i:119872;a:1:{i:0;i:109;}i:119873;a:1:{i:0;i:110;}i:119874;a:1:{i:0;i:111;}i:119875;a:1:{i:0;i:112;}i:119876;a:1:{i:0;i:113;}i:119877;a:1:{i:0;i:114;}i:119878;a:1:{i:0;i:115;}i:119879;a:1:{i:0;i:116;}i:119880;a:1:{i:0;i:117;}i:119881;a:1:{i:0;i:118;}i:119882;a:1:{i:0;i:119;}i:119883;a:1:{i:0;i:120;}i:119884;a:1:{i:0;i:121;}i:119885;a:1:{i:0;i:122;}i:119912;a:1:{i:0;i:97;}i:119913;a:1:{i:0;i:98;}i:119914;a:1:{i:0;i:99;}i:119915;a:1:{i:0;i:100;}i:119916;a:1:{i:0;i:101;}i:119917;a:1:{i:0;i:102;}i:119918;a:1:{i:0;i:103;}i:119919;a:1:{i:0;i:104;}i:119920;a:1:{i:0;i:105;}i:119921;a:1:{i:0;i:106;}i:119922;a:1:{i:0;i:107;}i:119923;a:1:{i:0;i:108;}i:119924;a:1:{i:0;i:109;}i:119925;a:1:{i:0;i:110;}i:119926;a:1:{i:0;i:111;}i:119927;a:1:{i:0;i:112;}i:119928;a:1:{i:0;i:113;}i:119929;a:1:{i:0;i:114;}i:119930;a:1:{i:0;i:115;}i:119931;a:1:{i:0;i:116;}i:119932;a:1:{i:0;i:117;}i:119933;a:1:{i:0;i:118;}i:119934;a:1:{i:0;i:119;}i:119935;a:1:{i:0;i:120;}i:119936;a:1:{i:0;i:121;}i:119937;a:1:{i:0;i:122;}i:119964;a:1:{i:0;i:97;}i:119966;a:1:{i:0;i:99;}i:119967;a:1:{i:0;i:100;}i:119970;a:1:{i:0;i:103;}i:119973;a:1:{i:0;i:106;}i:119974;a:1:{i:0;i:107;}i:119977;a:1:{i:0;i:110;}i:119978;a:1:{i:0;i:111;}i:119979;a:1:{i:0;i:112;}i:119980;a:1:{i:0;i:113;}i:119982;a:1:{i:0;i:115;}i:119983;a:1:{i:0;i:116;}i:119984;a:1:{i:0;i:117;}i:119985;a:1:{i:0;i:118;}i:119986;a:1:{i:0;i:119;}i:119987;a:1:{i:0;i:120;}i:119988;a:1:{i:0;i:121;}i:119989;a:1:{i:0;i:122;}i:120016;a:1:{i:0;i:97;}i:120017;a:1:{i:0;i:98;}i:120018;a:1:{i:0;i:99;}i:120019;a:1:{i:0;i:100;}i:120020;a:1:{i:0;i:101;}i:120021;a:1:{i:0;i:102;}i:120022;a:1:{i:0;i:103;}i:120023;a:1:{i:0;i:104;}i:120024;a:1:{i:0;i:105;}i:120025;a:1:{i:0;i:106;}i:120026;a:1:{i:0;i:107;}i:120027;a:1:{i:0;i:108;}i:120028;a:1:{i:0;i:109;}i:120029;a:1:{i:0;i:110;}i:120030;a:1:{i:0;i:111;}i:120031;a:1:{i:0;i:112;}i:120032;a:1:{i:0;i:113;}i:120033;a:1:{i:0;i:114;}i:120034;a:1:{i:0;i:115;}i:120035;a:1:{i:0;i:116;}i:120036;a:1:{i:0;i:117;}i:120037;a:1:{i:0;i:118;}i:120038;a:1:{i:0;i:119;}i:120039;a:1:{i:0;i:120;}i:120040;a:1:{i:0;i:121;}i:120041;a:1:{i:0;i:122;}i:120068;a:1:{i:0;i:97;}i:120069;a:1:{i:0;i:98;}i:120071;a:1:{i:0;i:100;}i:120072;a:1:{i:0;i:101;}i:120073;a:1:{i:0;i:102;}i:120074;a:1:{i:0;i:103;}i:120077;a:1:{i:0;i:106;}i:120078;a:1:{i:0;i:107;}i:120079;a:1:{i:0;i:108;}i:120080;a:1:{i:0;i:109;}i:120081;a:1:{i:0;i:110;}i:120082;a:1:{i:0;i:111;}i:120083;a:1:{i:0;i:112;}i:120084;a:1:{i:0;i:113;}i:120086;a:1:{i:0;i:115;}i:120087;a:1:{i:0;i:116;}i:120088;a:1:{i:0;i:117;}i:120089;a:1:{i:0;i:118;}i:120090;a:1:{i:0;i:119;}i:120091;a:1:{i:0;i:120;}i:120092;a:1:{i:0;i:121;}i:120120;a:1:{i:0;i:97;}i:120121;a:1:{i:0;i:98;}i:120123;a:1:{i:0;i:100;}i:120124;a:1:{i:0;i:101;}i:120125;a:1:{i:0;i:102;}i:120126;a:1:{i:0;i:103;}i:120128;a:1:{i:0;i:105;}i:120129;a:1:{i:0;i:106;}i:120130;a:1:{i:0;i:107;}i:120131;a:1:{i:0;i:108;}i:120132;a:1:{i:0;i:109;}i:120134;a:1:{i:0;i:111;}i:120138;a:1:{i:0;i:115;}i:120139;a:1:{i:0;i:116;}i:120140;a:1:{i:0;i:117;}i:120141;a:1:{i:0;i:118;}i:120142;a:1:{i:0;i:119;}i:120143;a:1:{i:0;i:120;}i:120144;a:1:{i:0;i:121;}i:120172;a:1:{i:0;i:97;}i:120173;a:1:{i:0;i:98;}i:120174;a:1:{i:0;i:99;}i:120175;a:1:{i:0;i:100;}i:120176;a:1:{i:0;i:101;}i:120177;a:1:{i:0;i:102;}i:120178;a:1:{i:0;i:103;}i:120179;a:1:{i:0;i:104;}i:120180;a:1:{i:0;i:105;}i:120181;a:1:{i:0;i:106;}i:120182;a:1:{i:0;i:107;}i:120183;a:1:{i:0;i:108;}i:120184;a:1:{i:0;i:109;}i:120185;a:1:{i:0;i:110;}i:120186;a:1:{i:0;i:111;}i:120187;a:1:{i:0;i:112;}i:120188;a:1:{i:0;i:113;}i:120189;a:1:{i:0;i:114;}i:120190;a:1:{i:0;i:115;}i:120191;a:1:{i:0;i:116;}i:120192;a:1:{i:0;i:117;}i:120193;a:1:{i:0;i:118;}i:120194;a:1:{i:0;i:119;}i:120195;a:1:{i:0;i:120;}i:120196;a:1:{i:0;i:121;}i:120197;a:1:{i:0;i:122;}i:120224;a:1:{i:0;i:97;}i:120225;a:1:{i:0;i:98;}i:120226;a:1:{i:0;i:99;}i:120227;a:1:{i:0;i:100;}i:120228;a:1:{i:0;i:101;}i:120229;a:1:{i:0;i:102;}i:120230;a:1:{i:0;i:103;}i:120231;a:1:{i:0;i:104;}i:120232;a:1:{i:0;i:105;}i:120233;a:1:{i:0;i:106;}i:120234;a:1:{i:0;i:107;}i:120235;a:1:{i:0;i:108;}i:120236;a:1:{i:0;i:109;}i:120237;a:1:{i:0;i:110;}i:120238;a:1:{i:0;i:111;}i:120239;a:1:{i:0;i:112;}i:120240;a:1:{i:0;i:113;}i:120241;a:1:{i:0;i:114;}i:120242;a:1:{i:0;i:115;}i:120243;a:1:{i:0;i:116;}i:120244;a:1:{i:0;i:117;}i:120245;a:1:{i:0;i:118;}i:120246;a:1:{i:0;i:119;}i:120247;a:1:{i:0;i:120;}i:120248;a:1:{i:0;i:121;}i:120249;a:1:{i:0;i:122;}i:120276;a:1:{i:0;i:97;}i:120277;a:1:{i:0;i:98;}i:120278;a:1:{i:0;i:99;}i:120279;a:1:{i:0;i:100;}i:120280;a:1:{i:0;i:101;}i:120281;a:1:{i:0;i:102;}i:120282;a:1:{i:0;i:103;}i:120283;a:1:{i:0;i:104;}i:120284;a:1:{i:0;i:105;}i:120285;a:1:{i:0;i:106;}i:120286;a:1:{i:0;i:107;}i:120287;a:1:{i:0;i:108;}i:120288;a:1:{i:0;i:109;}i:120289;a:1:{i:0;i:110;}i:120290;a:1:{i:0;i:111;}i:120291;a:1:{i:0;i:112;}i:120292;a:1:{i:0;i:113;}i:120293;a:1:{i:0;i:114;}i:120294;a:1:{i:0;i:115;}i:120295;a:1:{i:0;i:116;}i:120296;a:1:{i:0;i:117;}i:120297;a:1:{i:0;i:118;}i:120298;a:1:{i:0;i:119;}i:120299;a:1:{i:0;i:120;}i:120300;a:1:{i:0;i:121;}i:120301;a:1:{i:0;i:122;}i:120328;a:1:{i:0;i:97;}i:120329;a:1:{i:0;i:98;}i:120330;a:1:{i:0;i:99;}i:120331;a:1:{i:0;i:100;}i:120332;a:1:{i:0;i:101;}i:120333;a:1:{i:0;i:102;}i:120334;a:1:{i:0;i:103;}i:120335;a:1:{i:0;i:104;}i:120336;a:1:{i:0;i:105;}i:120337;a:1:{i:0;i:106;}i:120338;a:1:{i:0;i:107;}i:120339;a:1:{i:0;i:108;}i:120340;a:1:{i:0;i:109;}i:120341;a:1:{i:0;i:110;}i:120342;a:1:{i:0;i:111;}i:120343;a:1:{i:0;i:112;}i:120344;a:1:{i:0;i:113;}i:120345;a:1:{i:0;i:114;}i:120346;a:1:{i:0;i:115;}i:120347;a:1:{i:0;i:116;}i:120348;a:1:{i:0;i:117;}i:120349;a:1:{i:0;i:118;}i:120350;a:1:{i:0;i:119;}i:120351;a:1:{i:0;i:120;}i:120352;a:1:{i:0;i:121;}i:120353;a:1:{i:0;i:122;}i:120380;a:1:{i:0;i:97;}i:120381;a:1:{i:0;i:98;}i:120382;a:1:{i:0;i:99;}i:120383;a:1:{i:0;i:100;}i:120384;a:1:{i:0;i:101;}i:120385;a:1:{i:0;i:102;}i:120386;a:1:{i:0;i:103;}i:120387;a:1:{i:0;i:104;}i:120388;a:1:{i:0;i:105;}i:120389;a:1:{i:0;i:106;}i:120390;a:1:{i:0;i:107;}i:120391;a:1:{i:0;i:108;}i:120392;a:1:{i:0;i:109;}i:120393;a:1:{i:0;i:110;}i:120394;a:1:{i:0;i:111;}i:120395;a:1:{i:0;i:112;}i:120396;a:1:{i:0;i:113;}i:120397;a:1:{i:0;i:114;}i:120398;a:1:{i:0;i:115;}i:120399;a:1:{i:0;i:116;}i:120400;a:1:{i:0;i:117;}i:120401;a:1:{i:0;i:118;}i:120402;a:1:{i:0;i:119;}i:120403;a:1:{i:0;i:120;}i:120404;a:1:{i:0;i:121;}i:120405;a:1:{i:0;i:122;}i:120432;a:1:{i:0;i:97;}i:120433;a:1:{i:0;i:98;}i:120434;a:1:{i:0;i:99;}i:120435;a:1:{i:0;i:100;}i:120436;a:1:{i:0;i:101;}i:120437;a:1:{i:0;i:102;}i:120438;a:1:{i:0;i:103;}i:120439;a:1:{i:0;i:104;}i:120440;a:1:{i:0;i:105;}i:120441;a:1:{i:0;i:106;}i:120442;a:1:{i:0;i:107;}i:120443;a:1:{i:0;i:108;}i:120444;a:1:{i:0;i:109;}i:120445;a:1:{i:0;i:110;}i:120446;a:1:{i:0;i:111;}i:120447;a:1:{i:0;i:112;}i:120448;a:1:{i:0;i:113;}i:120449;a:1:{i:0;i:114;}i:120450;a:1:{i:0;i:115;}i:120451;a:1:{i:0;i:116;}i:120452;a:1:{i:0;i:117;}i:120453;a:1:{i:0;i:118;}i:120454;a:1:{i:0;i:119;}i:120455;a:1:{i:0;i:120;}i:120456;a:1:{i:0;i:121;}i:120457;a:1:{i:0;i:122;}i:120488;a:1:{i:0;i:945;}i:120489;a:1:{i:0;i:946;}i:120490;a:1:{i:0;i:947;}i:120491;a:1:{i:0;i:948;}i:120492;a:1:{i:0;i:949;}i:120493;a:1:{i:0;i:950;}i:120494;a:1:{i:0;i:951;}i:120495;a:1:{i:0;i:952;}i:120496;a:1:{i:0;i:953;}i:120497;a:1:{i:0;i:954;}i:120498;a:1:{i:0;i:955;}i:120499;a:1:{i:0;i:956;}i:120500;a:1:{i:0;i:957;}i:120501;a:1:{i:0;i:958;}i:120502;a:1:{i:0;i:959;}i:120503;a:1:{i:0;i:960;}i:120504;a:1:{i:0;i:961;}i:120505;a:1:{i:0;i:952;}i:120506;a:1:{i:0;i:963;}i:120507;a:1:{i:0;i:964;}i:120508;a:1:{i:0;i:965;}i:120509;a:1:{i:0;i:966;}i:120510;a:1:{i:0;i:967;}i:120511;a:1:{i:0;i:968;}i:120512;a:1:{i:0;i:969;}i:120531;a:1:{i:0;i:963;}i:120546;a:1:{i:0;i:945;}i:120547;a:1:{i:0;i:946;}i:120548;a:1:{i:0;i:947;}i:120549;a:1:{i:0;i:948;}i:120550;a:1:{i:0;i:949;}i:120551;a:1:{i:0;i:950;}i:120552;a:1:{i:0;i:951;}i:120553;a:1:{i:0;i:952;}i:120554;a:1:{i:0;i:953;}i:120555;a:1:{i:0;i:954;}i:120556;a:1:{i:0;i:955;}i:120557;a:1:{i:0;i:956;}i:120558;a:1:{i:0;i:957;}i:120559;a:1:{i:0;i:958;}i:120560;a:1:{i:0;i:959;}i:120561;a:1:{i:0;i:960;}i:120562;a:1:{i:0;i:961;}i:120563;a:1:{i:0;i:952;}i:120564;a:1:{i:0;i:963;}i:120565;a:1:{i:0;i:964;}i:120566;a:1:{i:0;i:965;}i:120567;a:1:{i:0;i:966;}i:120568;a:1:{i:0;i:967;}i:120569;a:1:{i:0;i:968;}i:120570;a:1:{i:0;i:969;}i:120589;a:1:{i:0;i:963;}i:120604;a:1:{i:0;i:945;}i:120605;a:1:{i:0;i:946;}i:120606;a:1:{i:0;i:947;}i:120607;a:1:{i:0;i:948;}i:120608;a:1:{i:0;i:949;}i:120609;a:1:{i:0;i:950;}i:120610;a:1:{i:0;i:951;}i:120611;a:1:{i:0;i:952;}i:120612;a:1:{i:0;i:953;}i:120613;a:1:{i:0;i:954;}i:120614;a:1:{i:0;i:955;}i:120615;a:1:{i:0;i:956;}i:120616;a:1:{i:0;i:957;}i:120617;a:1:{i:0;i:958;}i:120618;a:1:{i:0;i:959;}i:120619;a:1:{i:0;i:960;}i:120620;a:1:{i:0;i:961;}i:120621;a:1:{i:0;i:952;}i:120622;a:1:{i:0;i:963;}i:120623;a:1:{i:0;i:964;}i:120624;a:1:{i:0;i:965;}i:120625;a:1:{i:0;i:966;}i:120626;a:1:{i:0;i:967;}i:120627;a:1:{i:0;i:968;}i:120628;a:1:{i:0;i:969;}i:120647;a:1:{i:0;i:963;}i:120662;a:1:{i:0;i:945;}i:120663;a:1:{i:0;i:946;}i:120664;a:1:{i:0;i:947;}i:120665;a:1:{i:0;i:948;}i:120666;a:1:{i:0;i:949;}i:120667;a:1:{i:0;i:950;}i:120668;a:1:{i:0;i:951;}i:120669;a:1:{i:0;i:952;}i:120670;a:1:{i:0;i:953;}i:120671;a:1:{i:0;i:954;}i:120672;a:1:{i:0;i:955;}i:120673;a:1:{i:0;i:956;}i:120674;a:1:{i:0;i:957;}i:120675;a:1:{i:0;i:958;}i:120676;a:1:{i:0;i:959;}i:120677;a:1:{i:0;i:960;}i:120678;a:1:{i:0;i:961;}i:120679;a:1:{i:0;i:952;}i:120680;a:1:{i:0;i:963;}i:120681;a:1:{i:0;i:964;}i:120682;a:1:{i:0;i:965;}i:120683;a:1:{i:0;i:966;}i:120684;a:1:{i:0;i:967;}i:120685;a:1:{i:0;i:968;}i:120686;a:1:{i:0;i:969;}i:120705;a:1:{i:0;i:963;}i:120720;a:1:{i:0;i:945;}i:120721;a:1:{i:0;i:946;}i:120722;a:1:{i:0;i:947;}i:120723;a:1:{i:0;i:948;}i:120724;a:1:{i:0;i:949;}i:120725;a:1:{i:0;i:950;}i:120726;a:1:{i:0;i:951;}i:120727;a:1:{i:0;i:952;}i:120728;a:1:{i:0;i:953;}i:120729;a:1:{i:0;i:954;}i:120730;a:1:{i:0;i:955;}i:120731;a:1:{i:0;i:956;}i:120732;a:1:{i:0;i:957;}i:120733;a:1:{i:0;i:958;}i:120734;a:1:{i:0;i:959;}i:120735;a:1:{i:0;i:960;}i:120736;a:1:{i:0;i:961;}i:120737;a:1:{i:0;i:952;}i:120738;a:1:{i:0;i:963;}i:120739;a:1:{i:0;i:964;}i:120740;a:1:{i:0;i:965;}i:120741;a:1:{i:0;i:966;}i:120742;a:1:{i:0;i:967;}i:120743;a:1:{i:0;i:968;}i:120744;a:1:{i:0;i:969;}i:120763;a:1:{i:0;i:963;}i:1017;a:1:{i:0;i:963;}i:7468;a:1:{i:0;i:97;}i:7469;a:1:{i:0;i:230;}i:7470;a:1:{i:0;i:98;}i:7472;a:1:{i:0;i:100;}i:7473;a:1:{i:0;i:101;}i:7474;a:1:{i:0;i:477;}i:7475;a:1:{i:0;i:103;}i:7476;a:1:{i:0;i:104;}i:7477;a:1:{i:0;i:105;}i:7478;a:1:{i:0;i:106;}i:7479;a:1:{i:0;i:107;}i:7480;a:1:{i:0;i:108;}i:7481;a:1:{i:0;i:109;}i:7482;a:1:{i:0;i:110;}i:7484;a:1:{i:0;i:111;}i:7485;a:1:{i:0;i:547;}i:7486;a:1:{i:0;i:112;}i:7487;a:1:{i:0;i:114;}i:7488;a:1:{i:0;i:116;}i:7489;a:1:{i:0;i:117;}i:7490;a:1:{i:0;i:119;}i:8507;a:3:{i:0;i:102;i:1;i:97;i:2;i:120;}i:12880;a:3:{i:0;i:112;i:1;i:116;i:2;i:101;}i:13004;a:2:{i:0;i:104;i:1;i:103;}i:13006;a:2:{i:0;i:101;i:1;i:118;}i:13007;a:3:{i:0;i:108;i:1;i:116;i:2;i:100;}i:13178;a:2:{i:0;i:105;i:1;i:117;}i:13278;a:3:{i:0;i:118;i:1;i:8725;i:2;i:109;}i:13279;a:3:{i:0;i:97;i:1;i:8725;i:2;i:109;}}s:12:"norm_combcls";a:341:{i:820;i:1;i:821;i:1;i:822;i:1;i:823;i:1;i:824;i:1;i:2364;i:7;i:2492;i:7;i:2620;i:7;i:2748;i:7;i:2876;i:7;i:3260;i:7;i:4151;i:7;i:12441;i:8;i:12442;i:8;i:2381;i:9;i:2509;i:9;i:2637;i:9;i:2765;i:9;i:2893;i:9;i:3021;i:9;i:3149;i:9;i:3277;i:9;i:3405;i:9;i:3530;i:9;i:3642;i:9;i:3972;i:9;i:4153;i:9;i:5908;i:9;i:5940;i:9;i:6098;i:9;i:1456;i:10;i:1457;i:11;i:1458;i:12;i:1459;i:13;i:1460;i:14;i:1461;i:15;i:1462;i:16;i:1463;i:17;i:1464;i:18;i:1465;i:19;i:1467;i:20;i:1468;i:21;i:1469;i:22;i:1471;i:23;i:1473;i:24;i:1474;i:25;i:64286;i:26;i:1611;i:27;i:1612;i:28;i:1613;i:29;i:1614;i:30;i:1615;i:31;i:1616;i:32;i:1617;i:33;i:1618;i:34;i:1648;i:35;i:1809;i:36;i:3157;i:84;i:3158;i:91;i:3640;i:103;i:3641;i:103;i:3656;i:107;i:3657;i:107;i:3658;i:107;i:3659;i:107;i:3768;i:118;i:3769;i:118;i:3784;i:122;i:3785;i:122;i:3786;i:122;i:3787;i:122;i:3953;i:129;i:3954;i:130;i:3962;i:130;i:3963;i:130;i:3964;i:130;i:3965;i:130;i:3968;i:130;i:3956;i:132;i:801;i:202;i:802;i:202;i:807;i:202;i:808;i:202;i:795;i:216;i:3897;i:216;i:119141;i:216;i:119142;i:216;i:119150;i:216;i:119151;i:216;i:119152;i:216;i:119153;i:216;i:119154;i:216;i:12330;i:218;i:790;i:220;i:791;i:220;i:792;i:220;i:793;i:220;i:796;i:220;i:797;i:220;i:798;i:220;i:799;i:220;i:800;i:220;i:803;i:220;i:804;i:220;i:805;i:220;i:806;i:220;i:809;i:220;i:810;i:220;i:811;i:220;i:812;i:220;i:813;i:220;i:814;i:220;i:815;i:220;i:816;i:220;i:817;i:220;i:818;i:220;i:819;i:220;i:825;i:220;i:826;i:220;i:827;i:220;i:828;i:220;i:839;i:220;i:840;i:220;i:841;i:220;i:845;i:220;i:846;i:220;i:851;i:220;i:852;i:220;i:853;i:220;i:854;i:220;i:1425;i:220;i:1430;i:220;i:1435;i:220;i:1443;i:220;i:1444;i:220;i:1445;i:220;i:1446;i:220;i:1447;i:220;i:1450;i:220;i:1621;i:220;i:1622;i:220;i:1763;i:220;i:1770;i:220;i:1773;i:220;i:1841;i:220;i:1844;i:220;i:1847;i:220;i:1848;i:220;i:1849;i:220;i:1851;i:220;i:1852;i:220;i:1854;i:220;i:1858;i:220;i:1860;i:220;i:1862;i:220;i:1864;i:220;i:2386;i:220;i:3864;i:220;i:3865;i:220;i:3893;i:220;i:3895;i:220;i:4038;i:220;i:6459;i:220;i:8424;i:220;i:119163;i:220;i:119164;i:220;i:119165;i:220;i:119166;i:220;i:119167;i:220;i:119168;i:220;i:119169;i:220;i:119170;i:220;i:119178;i:220;i:119179;i:220;i:1434;i:222;i:1453;i:222;i:6441;i:222;i:12333;i:222;i:12334;i:224;i:12335;i:224;i:119149;i:226;i:1454;i:228;i:6313;i:228;i:12331;i:228;i:768;i:230;i:769;i:230;i:770;i:230;i:771;i:230;i:772;i:230;i:773;i:230;i:774;i:230;i:775;i:230;i:776;i:230;i:777;i:230;i:778;i:230;i:779;i:230;i:780;i:230;i:781;i:230;i:782;i:230;i:783;i:230;i:784;i:230;i:785;i:230;i:786;i:230;i:787;i:230;i:788;i:230;i:829;i:230;i:830;i:230;i:831;i:230;i:832;i:230;i:833;i:230;i:834;i:230;i:835;i:230;i:836;i:230;i:838;i:230;i:842;i:230;i:843;i:230;i:844;i:230;i:848;i:230;i:849;i:230;i:850;i:230;i:855;i:230;i:867;i:230;i:868;i:230;i:869;i:230;i:870;i:230;i:871;i:230;i:872;i:230;i:873;i:230;i:874;i:230;i:875;i:230;i:876;i:230;i:877;i:230;i:878;i:230;i:879;i:230;i:1155;i:230;i:1156;i:230;i:1157;i:230;i:1158;i:230;i:1426;i:230;i:1427;i:230;i:1428;i:230;i:1429;i:230;i:1431;i:230;i:1432;i:230;i:1433;i:230;i:1436;i:230;i:1437;i:230;i:1438;i:230;i:1439;i:230;i:1440;i:230;i:1441;i:230;i:1448;i:230;i:1449;i:230;i:1451;i:230;i:1452;i:230;i:1455;i:230;i:1476;i:230;i:1552;i:230;i:1553;i:230;i:1554;i:230;i:1555;i:230;i:1556;i:230;i:1557;i:230;i:1619;i:230;i:1620;i:230;i:1623;i:230;i:1624;i:230;i:1750;i:230;i:1751;i:230;i:1752;i:230;i:1753;i:230;i:1754;i:230;i:1755;i:230;i:1756;i:230;i:1759;i:230;i:1760;i:230;i:1761;i:230;i:1762;i:230;i:1764;i:230;i:1767;i:230;i:1768;i:230;i:1771;i:230;i:1772;i:230;i:1840;i:230;i:1842;i:230;i:1843;i:230;i:1845;i:230;i:1846;i:230;i:1850;i:230;i:1853;i:230;i:1855;i:230;i:1856;i:230;i:1857;i:230;i:1859;i:230;i:1861;i:230;i:1863;i:230;i:1865;i:230;i:1866;i:230;i:2385;i:230;i:2387;i:230;i:2388;i:230;i:3970;i:230;i:3971;i:230;i:3974;i:230;i:3975;i:230;i:5901;i:230;i:6458;i:230;i:8400;i:230;i:8401;i:230;i:8404;i:230;i:8405;i:230;i:8406;i:230;i:8407;i:230;i:8411;i:230;i:8412;i:230;i:8417;i:230;i:8423;i:230;i:8425;i:230;i:65056;i:230;i:65057;i:230;i:65058;i:230;i:65059;i:230;i:119173;i:230;i:119174;i:230;i:119175;i:230;i:119177;i:230;i:119176;i:230;i:119210;i:230;i:119211;i:230;i:119212;i:230;i:119213;i:230;i:789;i:232;i:794;i:232;i:12332;i:232;i:863;i:233;i:866;i:233;i:861;i:234;i:862;i:234;i:864;i:234;i:865;i:234;i:837;i:240;}}
\ No newline at end of file diff --git a/vendor/simplepie/simplepie/library/SimplePie.php b/vendor/simplepie/simplepie/library/SimplePie.php new file mode 100755 index 000000000..428bfc068 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie.php @@ -0,0 +1,3303 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2017, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @version 1.5 + * @copyright 2004-2017 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * SimplePie Name + */ +define('SIMPLEPIE_NAME', 'SimplePie'); + +/** + * SimplePie Version + */ +define('SIMPLEPIE_VERSION', '1.5'); + +/** + * SimplePie Build + * @todo Hardcode for release (there's no need to have to call SimplePie_Misc::get_build() only every load of simplepie.inc) + */ +define('SIMPLEPIE_BUILD', gmdate('YmdHis', SimplePie_Misc::get_build())); + +/** + * SimplePie Website URL + */ +define('SIMPLEPIE_URL', 'http://simplepie.org'); + +/** + * SimplePie Useragent + * @see SimplePie::set_useragent() + */ +define('SIMPLEPIE_USERAGENT', SIMPLEPIE_NAME . '/' . SIMPLEPIE_VERSION . ' (Feed Parser; ' . SIMPLEPIE_URL . '; Allow like Gecko) Build/' . SIMPLEPIE_BUILD); + +/** + * SimplePie Linkback + */ +define('SIMPLEPIE_LINKBACK', '<a href="' . SIMPLEPIE_URL . '" title="' . SIMPLEPIE_NAME . ' ' . SIMPLEPIE_VERSION . '">' . SIMPLEPIE_NAME . '</a>'); + +/** + * No Autodiscovery + * @see SimplePie::set_autodiscovery_level() + */ +define('SIMPLEPIE_LOCATOR_NONE', 0); + +/** + * Feed Link Element Autodiscovery + * @see SimplePie::set_autodiscovery_level() + */ +define('SIMPLEPIE_LOCATOR_AUTODISCOVERY', 1); + +/** + * Local Feed Extension Autodiscovery + * @see SimplePie::set_autodiscovery_level() + */ +define('SIMPLEPIE_LOCATOR_LOCAL_EXTENSION', 2); + +/** + * Local Feed Body Autodiscovery + * @see SimplePie::set_autodiscovery_level() + */ +define('SIMPLEPIE_LOCATOR_LOCAL_BODY', 4); + +/** + * Remote Feed Extension Autodiscovery + * @see SimplePie::set_autodiscovery_level() + */ +define('SIMPLEPIE_LOCATOR_REMOTE_EXTENSION', 8); + +/** + * Remote Feed Body Autodiscovery + * @see SimplePie::set_autodiscovery_level() + */ +define('SIMPLEPIE_LOCATOR_REMOTE_BODY', 16); + +/** + * All Feed Autodiscovery + * @see SimplePie::set_autodiscovery_level() + */ +define('SIMPLEPIE_LOCATOR_ALL', 31); + +/** + * No known feed type + */ +define('SIMPLEPIE_TYPE_NONE', 0); + +/** + * RSS 0.90 + */ +define('SIMPLEPIE_TYPE_RSS_090', 1); + +/** + * RSS 0.91 (Netscape) + */ +define('SIMPLEPIE_TYPE_RSS_091_NETSCAPE', 2); + +/** + * RSS 0.91 (Userland) + */ +define('SIMPLEPIE_TYPE_RSS_091_USERLAND', 4); + +/** + * RSS 0.91 (both Netscape and Userland) + */ +define('SIMPLEPIE_TYPE_RSS_091', 6); + +/** + * RSS 0.92 + */ +define('SIMPLEPIE_TYPE_RSS_092', 8); + +/** + * RSS 0.93 + */ +define('SIMPLEPIE_TYPE_RSS_093', 16); + +/** + * RSS 0.94 + */ +define('SIMPLEPIE_TYPE_RSS_094', 32); + +/** + * RSS 1.0 + */ +define('SIMPLEPIE_TYPE_RSS_10', 64); + +/** + * RSS 2.0 + */ +define('SIMPLEPIE_TYPE_RSS_20', 128); + +/** + * RDF-based RSS + */ +define('SIMPLEPIE_TYPE_RSS_RDF', 65); + +/** + * Non-RDF-based RSS (truly intended as syndication format) + */ +define('SIMPLEPIE_TYPE_RSS_SYNDICATION', 190); + +/** + * All RSS + */ +define('SIMPLEPIE_TYPE_RSS_ALL', 255); + +/** + * Atom 0.3 + */ +define('SIMPLEPIE_TYPE_ATOM_03', 256); + +/** + * Atom 1.0 + */ +define('SIMPLEPIE_TYPE_ATOM_10', 512); + +/** + * All Atom + */ +define('SIMPLEPIE_TYPE_ATOM_ALL', 768); + +/** + * All feed types + */ +define('SIMPLEPIE_TYPE_ALL', 1023); + +/** + * No construct + */ +define('SIMPLEPIE_CONSTRUCT_NONE', 0); + +/** + * Text construct + */ +define('SIMPLEPIE_CONSTRUCT_TEXT', 1); + +/** + * HTML construct + */ +define('SIMPLEPIE_CONSTRUCT_HTML', 2); + +/** + * XHTML construct + */ +define('SIMPLEPIE_CONSTRUCT_XHTML', 4); + +/** + * base64-encoded construct + */ +define('SIMPLEPIE_CONSTRUCT_BASE64', 8); + +/** + * IRI construct + */ +define('SIMPLEPIE_CONSTRUCT_IRI', 16); + +/** + * A construct that might be HTML + */ +define('SIMPLEPIE_CONSTRUCT_MAYBE_HTML', 32); + +/** + * All constructs + */ +define('SIMPLEPIE_CONSTRUCT_ALL', 63); + +/** + * Don't change case + */ +define('SIMPLEPIE_SAME_CASE', 1); + +/** + * Change to lowercase + */ +define('SIMPLEPIE_LOWERCASE', 2); + +/** + * Change to uppercase + */ +define('SIMPLEPIE_UPPERCASE', 4); + +/** + * PCRE for HTML attributes + */ +define('SIMPLEPIE_PCRE_HTML_ATTRIBUTE', '((?:[\x09\x0A\x0B\x0C\x0D\x20]+[^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3E][^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3D\x3E]*(?:[\x09\x0A\x0B\x0C\x0D\x20]*=[\x09\x0A\x0B\x0C\x0D\x20]*(?:"(?:[^"]*)"|\'(?:[^\']*)\'|(?:[^\x09\x0A\x0B\x0C\x0D\x20\x22\x27\x3E][^\x09\x0A\x0B\x0C\x0D\x20\x3E]*)?))?)*)[\x09\x0A\x0B\x0C\x0D\x20]*'); + +/** + * PCRE for XML attributes + */ +define('SIMPLEPIE_PCRE_XML_ATTRIBUTE', '((?:\s+(?:(?:[^\s:]+:)?[^\s:]+)\s*=\s*(?:"(?:[^"]*)"|\'(?:[^\']*)\'))*)\s*'); + +/** + * XML Namespace + */ +define('SIMPLEPIE_NAMESPACE_XML', 'http://www.w3.org/XML/1998/namespace'); + +/** + * Atom 1.0 Namespace + */ +define('SIMPLEPIE_NAMESPACE_ATOM_10', 'http://www.w3.org/2005/Atom'); + +/** + * Atom 0.3 Namespace + */ +define('SIMPLEPIE_NAMESPACE_ATOM_03', 'http://purl.org/atom/ns#'); + +/** + * RDF Namespace + */ +define('SIMPLEPIE_NAMESPACE_RDF', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'); + +/** + * RSS 0.90 Namespace + */ +define('SIMPLEPIE_NAMESPACE_RSS_090', 'http://my.netscape.com/rdf/simple/0.9/'); + +/** + * RSS 1.0 Namespace + */ +define('SIMPLEPIE_NAMESPACE_RSS_10', 'http://purl.org/rss/1.0/'); + +/** + * RSS 1.0 Content Module Namespace + */ +define('SIMPLEPIE_NAMESPACE_RSS_10_MODULES_CONTENT', 'http://purl.org/rss/1.0/modules/content/'); + +/** + * RSS 2.0 Namespace + * (Stupid, I know, but I'm certain it will confuse people less with support.) + */ +define('SIMPLEPIE_NAMESPACE_RSS_20', ''); + +/** + * DC 1.0 Namespace + */ +define('SIMPLEPIE_NAMESPACE_DC_10', 'http://purl.org/dc/elements/1.0/'); + +/** + * DC 1.1 Namespace + */ +define('SIMPLEPIE_NAMESPACE_DC_11', 'http://purl.org/dc/elements/1.1/'); + +/** + * W3C Basic Geo (WGS84 lat/long) Vocabulary Namespace + */ +define('SIMPLEPIE_NAMESPACE_W3C_BASIC_GEO', 'http://www.w3.org/2003/01/geo/wgs84_pos#'); + +/** + * GeoRSS Namespace + */ +define('SIMPLEPIE_NAMESPACE_GEORSS', 'http://www.georss.org/georss'); + +/** + * Media RSS Namespace + */ +define('SIMPLEPIE_NAMESPACE_MEDIARSS', 'http://search.yahoo.com/mrss/'); + +/** + * Wrong Media RSS Namespace. Caused by a long-standing typo in the spec. + */ +define('SIMPLEPIE_NAMESPACE_MEDIARSS_WRONG', 'http://search.yahoo.com/mrss'); + +/** + * Wrong Media RSS Namespace #2. New namespace introduced in Media RSS 1.5. + */ +define('SIMPLEPIE_NAMESPACE_MEDIARSS_WRONG2', 'http://video.search.yahoo.com/mrss'); + +/** + * Wrong Media RSS Namespace #3. A possible typo of the Media RSS 1.5 namespace. + */ +define('SIMPLEPIE_NAMESPACE_MEDIARSS_WRONG3', 'http://video.search.yahoo.com/mrss/'); + +/** + * Wrong Media RSS Namespace #4. New spec location after the RSS Advisory Board takes it over, but not a valid namespace. + */ +define('SIMPLEPIE_NAMESPACE_MEDIARSS_WRONG4', 'http://www.rssboard.org/media-rss'); + +/** + * Wrong Media RSS Namespace #5. A possible typo of the RSS Advisory Board URL. + */ +define('SIMPLEPIE_NAMESPACE_MEDIARSS_WRONG5', 'http://www.rssboard.org/media-rss/'); + +/** + * iTunes RSS Namespace + */ +define('SIMPLEPIE_NAMESPACE_ITUNES', 'http://www.itunes.com/dtds/podcast-1.0.dtd'); + +/** + * XHTML Namespace + */ +define('SIMPLEPIE_NAMESPACE_XHTML', 'http://www.w3.org/1999/xhtml'); + +/** + * IANA Link Relations Registry + */ +define('SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY', 'http://www.iana.org/assignments/relation/'); + +/** + * No file source + */ +define('SIMPLEPIE_FILE_SOURCE_NONE', 0); + +/** + * Remote file source + */ +define('SIMPLEPIE_FILE_SOURCE_REMOTE', 1); + +/** + * Local file source + */ +define('SIMPLEPIE_FILE_SOURCE_LOCAL', 2); + +/** + * fsockopen() file source + */ +define('SIMPLEPIE_FILE_SOURCE_FSOCKOPEN', 4); + +/** + * cURL file source + */ +define('SIMPLEPIE_FILE_SOURCE_CURL', 8); + +/** + * file_get_contents() file source + */ +define('SIMPLEPIE_FILE_SOURCE_FILE_GET_CONTENTS', 16); + + + +/** + * SimplePie + * + * @package SimplePie + * @subpackage API + */ +class SimplePie +{ + /** + * @var array Raw data + * @access private + */ + public $data = array(); + + /** + * @var mixed Error string + * @access private + */ + public $error; + + /** + * @var object Instance of SimplePie_Sanitize (or other class) + * @see SimplePie::set_sanitize_class() + * @access private + */ + public $sanitize; + + /** + * @var string SimplePie Useragent + * @see SimplePie::set_useragent() + * @access private + */ + public $useragent = SIMPLEPIE_USERAGENT; + + /** + * @var string Feed URL + * @see SimplePie::set_feed_url() + * @access private + */ + public $feed_url; + + /** + * @var string Original feed URL, or new feed URL iff HTTP 301 Moved Permanently + * @see SimplePie::subscribe_url() + * @access private + */ + public $permanent_url = null; + + /** + * @var object Instance of SimplePie_File to use as a feed + * @see SimplePie::set_file() + * @access private + */ + public $file; + + /** + * @var string Raw feed data + * @see SimplePie::set_raw_data() + * @access private + */ + public $raw_data; + + /** + * @var int Timeout for fetching remote files + * @see SimplePie::set_timeout() + * @access private + */ + public $timeout = 10; + + /** + * @var array Custom curl options + * @see SimplePie::set_curl_options() + * @access private + */ + public $curl_options = array(); + + /** + * @var bool Forces fsockopen() to be used for remote files instead + * of cURL, even if a new enough version is installed + * @see SimplePie::force_fsockopen() + * @access private + */ + public $force_fsockopen = false; + + /** + * @var bool Force the given data/URL to be treated as a feed no matter what + * it appears like + * @see SimplePie::force_feed() + * @access private + */ + public $force_feed = false; + + /** + * @var bool Enable/Disable Caching + * @see SimplePie::enable_cache() + * @access private + */ + public $cache = true; + + /** + * @var bool Force SimplePie to fallback to expired cache, if enabled, + * when feed is unavailable. + * @see SimplePie::force_cache_fallback() + * @access private + */ + public $force_cache_fallback = false; + + /** + * @var int Cache duration (in seconds) + * @see SimplePie::set_cache_duration() + * @access private + */ + public $cache_duration = 3600; + + /** + * @var int Auto-discovery cache duration (in seconds) + * @see SimplePie::set_autodiscovery_cache_duration() + * @access private + */ + public $autodiscovery_cache_duration = 604800; // 7 Days. + + /** + * @var string Cache location (relative to executing script) + * @see SimplePie::set_cache_location() + * @access private + */ + public $cache_location = './cache'; + + /** + * @var string Function that creates the cache filename + * @see SimplePie::set_cache_name_function() + * @access private + */ + public $cache_name_function = 'md5'; + + /** + * @var bool Reorder feed by date descending + * @see SimplePie::enable_order_by_date() + * @access private + */ + public $order_by_date = true; + + /** + * @var mixed Force input encoding to be set to the follow value + * (false, or anything type-cast to false, disables this feature) + * @see SimplePie::set_input_encoding() + * @access private + */ + public $input_encoding = false; + + /** + * @var int Feed Autodiscovery Level + * @see SimplePie::set_autodiscovery_level() + * @access private + */ + public $autodiscovery = SIMPLEPIE_LOCATOR_ALL; + + /** + * Class registry object + * + * @var SimplePie_Registry + */ + public $registry; + + /** + * @var int Maximum number of feeds to check with autodiscovery + * @see SimplePie::set_max_checked_feeds() + * @access private + */ + public $max_checked_feeds = 10; + + /** + * @var array All the feeds found during the autodiscovery process + * @see SimplePie::get_all_discovered_feeds() + * @access private + */ + public $all_discovered_feeds = array(); + + /** + * @var string Web-accessible path to the handler_image.php file. + * @see SimplePie::set_image_handler() + * @access private + */ + public $image_handler = ''; + + /** + * @var array Stores the URLs when multiple feeds are being initialized. + * @see SimplePie::set_feed_url() + * @access private + */ + public $multifeed_url = array(); + + /** + * @var array Stores SimplePie objects when multiple feeds initialized. + * @access private + */ + public $multifeed_objects = array(); + + /** + * @var array Stores the get_object_vars() array for use with multifeeds. + * @see SimplePie::set_feed_url() + * @access private + */ + public $config_settings = null; + + /** + * @var integer Stores the number of items to return per-feed with multifeeds. + * @see SimplePie::set_item_limit() + * @access private + */ + public $item_limit = 0; + + /** + * @var bool Stores if last-modified and/or etag headers were sent with the + * request when checking a feed. + */ + public $check_modified = false; + + /** + * @var array Stores the default attributes to be stripped by strip_attributes(). + * @see SimplePie::strip_attributes() + * @access private + */ + public $strip_attributes = array('bgsound', 'class', 'expr', 'id', 'style', 'onclick', 'onerror', 'onfinish', 'onmouseover', 'onmouseout', 'onfocus', 'onblur', 'lowsrc', 'dynsrc'); + + /** + * @var array Stores the default attributes to add to different tags by add_attributes(). + * @see SimplePie::add_attributes() + * @access private + */ + public $add_attributes = array('audio' => array('preload' => 'none'), 'iframe' => array('sandbox' => 'allow-scripts allow-same-origin'), 'video' => array('preload' => 'none')); + + /** + * @var array Stores the default tags to be stripped by strip_htmltags(). + * @see SimplePie::strip_htmltags() + * @access private + */ + public $strip_htmltags = array('base', 'blink', 'body', 'doctype', 'embed', 'font', 'form', 'frame', 'frameset', 'html', 'iframe', 'input', 'marquee', 'meta', 'noscript', 'object', 'param', 'script', 'style'); + + /** + * @var bool Should we throw exceptions, or use the old-style error property? + * @access private + */ + public $enable_exceptions = false; + + /** + * The SimplePie class contains feed level data and options + * + * To use SimplePie, create the SimplePie object with no parameters. You can + * then set configuration options using the provided methods. After setting + * them, you must initialise the feed using $feed->init(). At that point the + * object's methods and properties will be available to you. + * + * Previously, it was possible to pass in the feed URL along with cache + * options directly into the constructor. This has been removed as of 1.3 as + * it caused a lot of confusion. + * + * @since 1.0 Preview Release + */ + public function __construct() + { + if (version_compare(PHP_VERSION, '5.3', '<')) + { + trigger_error('Please upgrade to PHP 5.3 or newer.'); + die(); + } + + // Other objects, instances created here so we can set options on them + $this->sanitize = new SimplePie_Sanitize(); + $this->registry = new SimplePie_Registry(); + + if (func_num_args() > 0) + { + $level = defined('E_USER_DEPRECATED') ? E_USER_DEPRECATED : E_USER_WARNING; + trigger_error('Passing parameters to the constructor is no longer supported. Please use set_feed_url(), set_cache_location(), and set_cache_duration() directly.', $level); + + $args = func_get_args(); + switch (count($args)) { + case 3: + $this->set_cache_duration($args[2]); + case 2: + $this->set_cache_location($args[1]); + case 1: + $this->set_feed_url($args[0]); + $this->init(); + } + } + } + + /** + * Used for converting object to a string + */ + public function __toString() + { + return md5(serialize($this->data)); + } + + /** + * Remove items that link back to this before destroying this object + */ + public function __destruct() + { + if ((version_compare(PHP_VERSION, '5.3', '<') || !gc_enabled()) && !ini_get('zend.ze1_compatibility_mode')) + { + if (!empty($this->data['items'])) + { + foreach ($this->data['items'] as $item) + { + $item->__destruct(); + } + unset($item, $this->data['items']); + } + if (!empty($this->data['ordered_items'])) + { + foreach ($this->data['ordered_items'] as $item) + { + $item->__destruct(); + } + unset($item, $this->data['ordered_items']); + } + } + } + + /** + * Force the given data/URL to be treated as a feed + * + * This tells SimplePie to ignore the content-type provided by the server. + * Be careful when using this option, as it will also disable autodiscovery. + * + * @since 1.1 + * @param bool $enable Force the given data/URL to be treated as a feed + */ + public function force_feed($enable = false) + { + $this->force_feed = (bool) $enable; + } + + /** + * Set the URL of the feed you want to parse + * + * This allows you to enter the URL of the feed you want to parse, or the + * website you want to try to use auto-discovery on. This takes priority + * over any set raw data. + * + * You can set multiple feeds to mash together by passing an array instead + * of a string for the $url. Remember that with each additional feed comes + * additional processing and resources. + * + * @since 1.0 Preview Release + * @see set_raw_data() + * @param string|array $url This is the URL (or array of URLs) that you want to parse. + */ + public function set_feed_url($url) + { + $this->multifeed_url = array(); + if (is_array($url)) + { + foreach ($url as $value) + { + $this->multifeed_url[] = $this->registry->call('Misc', 'fix_protocol', array($value, 1)); + } + } + else + { + $this->feed_url = $this->registry->call('Misc', 'fix_protocol', array($url, 1)); + $this->permanent_url = $this->feed_url; + } + } + + /** + * Set an instance of {@see SimplePie_File} to use as a feed + * + * @param SimplePie_File &$file + * @return bool True on success, false on failure + */ + public function set_file(&$file) + { + if ($file instanceof SimplePie_File) + { + $this->feed_url = $file->url; + $this->permanent_url = $this->feed_url; + $this->file =& $file; + return true; + } + return false; + } + + /** + * Set the raw XML data to parse + * + * Allows you to use a string of RSS/Atom data instead of a remote feed. + * + * If you have a feed available as a string in PHP, you can tell SimplePie + * to parse that data string instead of a remote feed. Any set feed URL + * takes precedence. + * + * @since 1.0 Beta 3 + * @param string $data RSS or Atom data as a string. + * @see set_feed_url() + */ + public function set_raw_data($data) + { + $this->raw_data = $data; + } + + /** + * Set the the default timeout for fetching remote feeds + * + * This allows you to change the maximum time the feed's server to respond + * and send the feed back. + * + * @since 1.0 Beta 3 + * @param int $timeout The maximum number of seconds to spend waiting to retrieve a feed. + */ + public function set_timeout($timeout = 10) + { + $this->timeout = (int) $timeout; + } + + /** + * Set custom curl options + * + * This allows you to change default curl options + * + * @since 1.0 Beta 3 + * @param array $curl_options Curl options to add to default settings + */ + public function set_curl_options(array $curl_options = array()) + { + $this->curl_options = $curl_options; + } + + /** + * Force SimplePie to use fsockopen() instead of cURL + * + * @since 1.0 Beta 3 + * @param bool $enable Force fsockopen() to be used + */ + public function force_fsockopen($enable = false) + { + $this->force_fsockopen = (bool) $enable; + } + + /** + * Enable/disable caching in SimplePie. + * + * This option allows you to disable caching all-together in SimplePie. + * However, disabling the cache can lead to longer load times. + * + * @since 1.0 Preview Release + * @param bool $enable Enable caching + */ + public function enable_cache($enable = true) + { + $this->cache = (bool) $enable; + } + + /** + * SimplePie to continue to fall back to expired cache, if enabled, when + * feed is unavailable. + * + * This tells SimplePie to ignore any file errors and fall back to cache + * instead. This only works if caching is enabled and cached content + * still exists. + + * @param bool $enable Force use of cache on fail. + */ + public function force_cache_fallback($enable = false) + { + $this->force_cache_fallback= (bool) $enable; + } + + /** + * Set the length of time (in seconds) that the contents of a feed will be + * cached + * + * @param int $seconds The feed content cache duration + */ + public function set_cache_duration($seconds = 3600) + { + $this->cache_duration = (int) $seconds; + } + + /** + * Set the length of time (in seconds) that the autodiscovered feed URL will + * be cached + * + * @param int $seconds The autodiscovered feed URL cache duration. + */ + public function set_autodiscovery_cache_duration($seconds = 604800) + { + $this->autodiscovery_cache_duration = (int) $seconds; + } + + /** + * Set the file system location where the cached files should be stored + * + * @param string $location The file system location. + */ + public function set_cache_location($location = './cache') + { + $this->cache_location = (string) $location; + } + + /** + * Set whether feed items should be sorted into reverse chronological order + * + * @param bool $enable Sort as reverse chronological order. + */ + public function enable_order_by_date($enable = true) + { + $this->order_by_date = (bool) $enable; + } + + /** + * Set the character encoding used to parse the feed + * + * This overrides the encoding reported by the feed, however it will fall + * back to the normal encoding detection if the override fails + * + * @param string $encoding Character encoding + */ + public function set_input_encoding($encoding = false) + { + if ($encoding) + { + $this->input_encoding = (string) $encoding; + } + else + { + $this->input_encoding = false; + } + } + + /** + * Set how much feed autodiscovery to do + * + * @see SIMPLEPIE_LOCATOR_NONE + * @see SIMPLEPIE_LOCATOR_AUTODISCOVERY + * @see SIMPLEPIE_LOCATOR_LOCAL_EXTENSION + * @see SIMPLEPIE_LOCATOR_LOCAL_BODY + * @see SIMPLEPIE_LOCATOR_REMOTE_EXTENSION + * @see SIMPLEPIE_LOCATOR_REMOTE_BODY + * @see SIMPLEPIE_LOCATOR_ALL + * @param int $level Feed Autodiscovery Level (level can be a combination of the above constants, see bitwise OR operator) + */ + public function set_autodiscovery_level($level = SIMPLEPIE_LOCATOR_ALL) + { + $this->autodiscovery = (int) $level; + } + + /** + * Get the class registry + * + * Use this to override SimplePie's default classes + * @see SimplePie_Registry + * @return SimplePie_Registry + */ + public function &get_registry() + { + return $this->registry; + } + + /**#@+ + * Useful when you are overloading or extending SimplePie's default classes. + * + * @deprecated Use {@see get_registry()} instead + * @link http://php.net/manual/en/language.oop5.basic.php#language.oop5.basic.extends PHP5 extends documentation + * @param string $class Name of custom class + * @return boolean True on success, false otherwise + */ + /** + * Set which class SimplePie uses for caching + */ + public function set_cache_class($class = 'SimplePie_Cache') + { + return $this->registry->register('Cache', $class, true); + } + + /** + * Set which class SimplePie uses for auto-discovery + */ + public function set_locator_class($class = 'SimplePie_Locator') + { + return $this->registry->register('Locator', $class, true); + } + + /** + * Set which class SimplePie uses for XML parsing + */ + public function set_parser_class($class = 'SimplePie_Parser') + { + return $this->registry->register('Parser', $class, true); + } + + /** + * Set which class SimplePie uses for remote file fetching + */ + public function set_file_class($class = 'SimplePie_File') + { + return $this->registry->register('File', $class, true); + } + + /** + * Set which class SimplePie uses for data sanitization + */ + public function set_sanitize_class($class = 'SimplePie_Sanitize') + { + return $this->registry->register('Sanitize', $class, true); + } + + /** + * Set which class SimplePie uses for handling feed items + */ + public function set_item_class($class = 'SimplePie_Item') + { + return $this->registry->register('Item', $class, true); + } + + /** + * Set which class SimplePie uses for handling author data + */ + public function set_author_class($class = 'SimplePie_Author') + { + return $this->registry->register('Author', $class, true); + } + + /** + * Set which class SimplePie uses for handling category data + */ + public function set_category_class($class = 'SimplePie_Category') + { + return $this->registry->register('Category', $class, true); + } + + /** + * Set which class SimplePie uses for feed enclosures + */ + public function set_enclosure_class($class = 'SimplePie_Enclosure') + { + return $this->registry->register('Enclosure', $class, true); + } + + /** + * Set which class SimplePie uses for `<media:text>` captions + */ + public function set_caption_class($class = 'SimplePie_Caption') + { + return $this->registry->register('Caption', $class, true); + } + + /** + * Set which class SimplePie uses for `<media:copyright>` + */ + public function set_copyright_class($class = 'SimplePie_Copyright') + { + return $this->registry->register('Copyright', $class, true); + } + + /** + * Set which class SimplePie uses for `<media:credit>` + */ + public function set_credit_class($class = 'SimplePie_Credit') + { + return $this->registry->register('Credit', $class, true); + } + + /** + * Set which class SimplePie uses for `<media:rating>` + */ + public function set_rating_class($class = 'SimplePie_Rating') + { + return $this->registry->register('Rating', $class, true); + } + + /** + * Set which class SimplePie uses for `<media:restriction>` + */ + public function set_restriction_class($class = 'SimplePie_Restriction') + { + return $this->registry->register('Restriction', $class, true); + } + + /** + * Set which class SimplePie uses for content-type sniffing + */ + public function set_content_type_sniffer_class($class = 'SimplePie_Content_Type_Sniffer') + { + return $this->registry->register('Content_Type_Sniffer', $class, true); + } + + /** + * Set which class SimplePie uses item sources + */ + public function set_source_class($class = 'SimplePie_Source') + { + return $this->registry->register('Source', $class, true); + } + /**#@-*/ + + /** + * Set the user agent string + * + * @param string $ua New user agent string. + */ + public function set_useragent($ua = SIMPLEPIE_USERAGENT) + { + $this->useragent = (string) $ua; + } + + /** + * Set callback function to create cache filename with + * + * @param mixed $function Callback function + */ + public function set_cache_name_function($function = 'md5') + { + if (is_callable($function)) + { + $this->cache_name_function = $function; + } + } + + /** + * Set options to make SP as fast as possible + * + * Forgoes a substantial amount of data sanitization in favor of speed. This + * turns SimplePie into a dumb parser of feeds. + * + * @param bool $set Whether to set them or not + */ + public function set_stupidly_fast($set = false) + { + if ($set) + { + $this->enable_order_by_date(false); + $this->remove_div(false); + $this->strip_comments(false); + $this->strip_htmltags(false); + $this->strip_attributes(false); + $this->add_attributes(false); + $this->set_image_handler(false); + } + } + + /** + * Set maximum number of feeds to check with autodiscovery + * + * @param int $max Maximum number of feeds to check + */ + public function set_max_checked_feeds($max = 10) + { + $this->max_checked_feeds = (int) $max; + } + + public function remove_div($enable = true) + { + $this->sanitize->remove_div($enable); + } + + public function strip_htmltags($tags = '', $encode = null) + { + if ($tags === '') + { + $tags = $this->strip_htmltags; + } + $this->sanitize->strip_htmltags($tags); + if ($encode !== null) + { + $this->sanitize->encode_instead_of_strip($tags); + } + } + + public function encode_instead_of_strip($enable = true) + { + $this->sanitize->encode_instead_of_strip($enable); + } + + public function strip_attributes($attribs = '') + { + if ($attribs === '') + { + $attribs = $this->strip_attributes; + } + $this->sanitize->strip_attributes($attribs); + } + + public function add_attributes($attribs = '') + { + if ($attribs === '') + { + $attribs = $this->add_attributes; + } + $this->sanitize->add_attributes($attribs); + } + + /** + * Set the output encoding + * + * Allows you to override SimplePie's output to match that of your webpage. + * This is useful for times when your webpages are not being served as + * UTF-8. This setting will be obeyed by {@see handle_content_type()}, and + * is similar to {@see set_input_encoding()}. + * + * It should be noted, however, that not all character encodings can support + * all characters. If your page is being served as ISO-8859-1 and you try + * to display a Japanese feed, you'll likely see garbled characters. + * Because of this, it is highly recommended to ensure that your webpages + * are served as UTF-8. + * + * The number of supported character encodings depends on whether your web + * host supports {@link http://php.net/mbstring mbstring}, + * {@link http://php.net/iconv iconv}, or both. See + * {@link http://simplepie.org/wiki/faq/Supported_Character_Encodings} for + * more information. + * + * @param string $encoding + */ + public function set_output_encoding($encoding = 'UTF-8') + { + $this->sanitize->set_output_encoding($encoding); + } + + public function strip_comments($strip = false) + { + $this->sanitize->strip_comments($strip); + } + + /** + * Set element/attribute key/value pairs of HTML attributes + * containing URLs that need to be resolved relative to the feed + * + * Defaults to |a|@href, |area|@href, |blockquote|@cite, |del|@cite, + * |form|@action, |img|@longdesc, |img|@src, |input|@src, |ins|@cite, + * |q|@cite + * + * @since 1.0 + * @param array|null $element_attribute Element/attribute key/value pairs, null for default + */ + public function set_url_replacements($element_attribute = null) + { + $this->sanitize->set_url_replacements($element_attribute); + } + + /** + * Set the handler to enable the display of cached images. + * + * @param str $page Web-accessible path to the handler_image.php file. + * @param str $qs The query string that the value should be passed to. + */ + public function set_image_handler($page = false, $qs = 'i') + { + if ($page !== false) + { + $this->sanitize->set_image_handler($page . '?' . $qs . '='); + } + else + { + $this->image_handler = ''; + } + } + + /** + * Set the limit for items returned per-feed with multifeeds + * + * @param integer $limit The maximum number of items to return. + */ + public function set_item_limit($limit = 0) + { + $this->item_limit = (int) $limit; + } + + /** + * Enable throwing exceptions + * + * @param boolean $enable Should we throw exceptions, or use the old-style error property? + */ + public function enable_exceptions($enable = true) + { + $this->enable_exceptions = $enable; + } + + /** + * Initialize the feed object + * + * This is what makes everything happen. Period. This is where all of the + * configuration options get processed, feeds are fetched, cached, and + * parsed, and all of that other good stuff. + * + * @return boolean True if successful, false otherwise + */ + public function init() + { + // Check absolute bare minimum requirements. + if (!extension_loaded('xml') || !extension_loaded('pcre')) + { + $this->error = 'XML or PCRE extensions not loaded!'; + return false; + } + // Then check the xml extension is sane (i.e., libxml 2.7.x issue on PHP < 5.2.9 and libxml 2.7.0 to 2.7.2 on any version) if we don't have xmlreader. + elseif (!extension_loaded('xmlreader')) + { + static $xml_is_sane = null; + if ($xml_is_sane === null) + { + $parser_check = xml_parser_create(); + xml_parse_into_struct($parser_check, '<foo>&</foo>', $values); + xml_parser_free($parser_check); + $xml_is_sane = isset($values[0]['value']); + } + if (!$xml_is_sane) + { + return false; + } + } + + if (method_exists($this->sanitize, 'set_registry')) + { + $this->sanitize->set_registry($this->registry); + } + + // Pass whatever was set with config options over to the sanitizer. + // Pass the classes in for legacy support; new classes should use the registry instead + $this->sanitize->pass_cache_data($this->cache, $this->cache_location, $this->cache_name_function, $this->registry->get_class('Cache')); + $this->sanitize->pass_file_data($this->registry->get_class('File'), $this->timeout, $this->useragent, $this->force_fsockopen, $this->curl_options); + + if (!empty($this->multifeed_url)) + { + $i = 0; + $success = 0; + $this->multifeed_objects = array(); + $this->error = array(); + foreach ($this->multifeed_url as $url) + { + $this->multifeed_objects[$i] = clone $this; + $this->multifeed_objects[$i]->set_feed_url($url); + $single_success = $this->multifeed_objects[$i]->init(); + $success |= $single_success; + if (!$single_success) + { + $this->error[$i] = $this->multifeed_objects[$i]->error(); + } + $i++; + } + return (bool) $success; + } + elseif ($this->feed_url === null && $this->raw_data === null) + { + return false; + } + + $this->error = null; + $this->data = array(); + $this->check_modified = false; + $this->multifeed_objects = array(); + $cache = false; + + if ($this->feed_url !== null) + { + $parsed_feed_url = $this->registry->call('Misc', 'parse_url', array($this->feed_url)); + + // Decide whether to enable caching + if ($this->cache && $parsed_feed_url['scheme'] !== '') + { + $cache = $this->registry->call('Cache', 'get_handler', array($this->cache_location, call_user_func($this->cache_name_function, $this->feed_url), 'spc')); + } + + // Fetch the data via SimplePie_File into $this->raw_data + if (($fetched = $this->fetch_data($cache)) === true) + { + return true; + } + elseif ($fetched === false) { + return false; + } + + list($headers, $sniffed) = $fetched; + } + + // Empty response check + if(empty($this->raw_data)){ + $this->error = "A feed could not be found at `$this->feed_url`. Empty body."; + $this->registry->call('Misc', 'error', array($this->error, E_USER_NOTICE, __FILE__, __LINE__)); + return false; + } + + // Set up array of possible encodings + $encodings = array(); + + // First check to see if input has been overridden. + if ($this->input_encoding !== false) + { + $encodings[] = strtoupper($this->input_encoding); + } + + $application_types = array('application/xml', 'application/xml-dtd', 'application/xml-external-parsed-entity'); + $text_types = array('text/xml', 'text/xml-external-parsed-entity'); + + // RFC 3023 (only applies to sniffed content) + if (isset($sniffed)) + { + if (in_array($sniffed, $application_types) || substr($sniffed, 0, 12) === 'application/' && substr($sniffed, -4) === '+xml') + { + if (isset($headers['content-type']) && preg_match('/;\x20?charset=([^;]*)/i', $headers['content-type'], $charset)) + { + $encodings[] = strtoupper($charset[1]); + } + $encodings = array_merge($encodings, $this->registry->call('Misc', 'xml_encoding', array($this->raw_data, &$this->registry))); + $encodings[] = 'UTF-8'; + } + elseif (in_array($sniffed, $text_types) || substr($sniffed, 0, 5) === 'text/' && substr($sniffed, -4) === '+xml') + { + if (isset($headers['content-type']) && preg_match('/;\x20?charset=([^;]*)/i', $headers['content-type'], $charset)) + { + $encodings[] = strtoupper($charset[1]); + } + $encodings[] = 'US-ASCII'; + } + // Text MIME-type default + elseif (substr($sniffed, 0, 5) === 'text/') + { + $encodings[] = 'UTF-8'; + } + } + + // Fallback to XML 1.0 Appendix F.1/UTF-8/ISO-8859-1 + $encodings = array_merge($encodings, $this->registry->call('Misc', 'xml_encoding', array($this->raw_data, &$this->registry))); + $encodings[] = 'UTF-8'; + $encodings[] = 'ISO-8859-1'; + + // There's no point in trying an encoding twice + $encodings = array_unique($encodings); + + // Loop through each possible encoding, till we return something, or run out of possibilities + foreach ($encodings as $encoding) + { + // Change the encoding to UTF-8 (as we always use UTF-8 internally) + if ($utf8_data = $this->registry->call('Misc', 'change_encoding', array($this->raw_data, $encoding, 'UTF-8'))) + { + // Create new parser + $parser = $this->registry->create('Parser'); + + // If it's parsed fine + if ($parser->parse($utf8_data, 'UTF-8', $this->permanent_url)) + { + $this->data = $parser->get_data(); + if (!($this->get_type() & ~SIMPLEPIE_TYPE_NONE)) + { + $this->error = "A feed could not be found at `$this->feed_url`. This does not appear to be a valid RSS or Atom feed."; + $this->registry->call('Misc', 'error', array($this->error, E_USER_NOTICE, __FILE__, __LINE__)); + return false; + } + + if (isset($headers)) + { + $this->data['headers'] = $headers; + } + $this->data['build'] = SIMPLEPIE_BUILD; + + // Cache the file if caching is enabled + if ($cache && !$cache->save($this)) + { + trigger_error("$this->cache_location is not writeable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING); + } + return true; + } + } + } + + if (isset($parser)) + { + // We have an error, just set SimplePie_Misc::error to it and quit + $this->error = $this->feed_url; + $this->error .= sprintf(' is invalid XML, likely due to invalid characters. XML error: %s at line %d, column %d', $parser->get_error_string(), $parser->get_current_line(), $parser->get_current_column()); + } + else + { + $this->error = 'The data could not be converted to UTF-8.'; + if (!extension_loaded('mbstring') && !extension_loaded('iconv') && !class_exists('\UConverter')) { + $this->error .= ' You MUST have either the iconv, mbstring or intl (PHP 5.5+) extension installed and enabled.'; + } else { + $missingExtensions = array(); + if (!extension_loaded('iconv')) { + $missingExtensions[] = 'iconv'; + } + if (!extension_loaded('mbstring')) { + $missingExtensions[] = 'mbstring'; + } + if (!class_exists('\UConverter')) { + $missingExtensions[] = 'intl (PHP 5.5+)'; + } + $this->error .= ' Try installing/enabling the ' . implode(' or ', $missingExtensions) . ' extension.'; + } + } + + $this->registry->call('Misc', 'error', array($this->error, E_USER_NOTICE, __FILE__, __LINE__)); + + return false; + } + + /** + * Fetch the data via SimplePie_File + * + * If the data is already cached, attempt to fetch it from there instead + * @param SimplePie_Cache|false $cache Cache handler, or false to not load from the cache + * @return array|true Returns true if the data was loaded from the cache, or an array of HTTP headers and sniffed type + */ + protected function fetch_data(&$cache) + { + // If it's enabled, use the cache + if ($cache) + { + // Load the Cache + $this->data = $cache->load(); + if (!empty($this->data)) + { + // If the cache is for an outdated build of SimplePie + if (!isset($this->data['build']) || $this->data['build'] !== SIMPLEPIE_BUILD) + { + $cache->unlink(); + $this->data = array(); + } + // If we've hit a collision just rerun it with caching disabled + elseif (isset($this->data['url']) && $this->data['url'] !== $this->feed_url) + { + $cache = false; + $this->data = array(); + } + // If we've got a non feed_url stored (if the page isn't actually a feed, or is a redirect) use that URL. + elseif (isset($this->data['feed_url'])) + { + // If the autodiscovery cache is still valid use it. + if ($cache->mtime() + $this->autodiscovery_cache_duration > time()) + { + // Do not need to do feed autodiscovery yet. + if ($this->data['feed_url'] !== $this->data['url']) + { + $this->set_feed_url($this->data['feed_url']); + return $this->init(); + } + + $cache->unlink(); + $this->data = array(); + } + } + // Check if the cache has been updated + elseif ($cache->mtime() + $this->cache_duration < time()) + { + // Want to know if we tried to send last-modified and/or etag headers + // when requesting this file. (Note that it's up to the file to + // support this, but we don't always send the headers either.) + $this->check_modified = true; + if (isset($this->data['headers']['last-modified']) || isset($this->data['headers']['etag'])) + { + $headers = array( + 'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1', + ); + if (isset($this->data['headers']['last-modified'])) + { + $headers['if-modified-since'] = $this->data['headers']['last-modified']; + } + if (isset($this->data['headers']['etag'])) + { + $headers['if-none-match'] = $this->data['headers']['etag']; + } + + $file = $this->registry->create('File', array($this->feed_url, $this->timeout/10, 5, $headers, $this->useragent, $this->force_fsockopen, $this->curl_options)); + + if ($file->success) + { + if ($file->status_code === 304) + { + // Set raw_data to false here too, to signify that the cache + // is still valid. + $this->raw_data = false; + $cache->touch(); + return true; + } + } + else + { + $this->check_modified = false; + if($this->force_cache_fallback) + { + $cache->touch(); + return true; + } + + unset($file); + } + } + } + // If the cache is still valid, just return true + else + { + $this->raw_data = false; + return true; + } + } + // If the cache is empty, delete it + else + { + $cache->unlink(); + $this->data = array(); + } + } + // If we don't already have the file (it'll only exist if we've opened it to check if the cache has been modified), open it. + if (!isset($file)) + { + if ($this->file instanceof SimplePie_File && $this->file->url === $this->feed_url) + { + $file =& $this->file; + } + else + { + $headers = array( + 'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1', + ); + $file = $this->registry->create('File', array($this->feed_url, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen, $this->curl_options)); + } + } + // If the file connection has an error, set SimplePie::error to that and quit + if (!$file->success && !($file->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0 || ($file->status_code === 200 || $file->status_code > 206 && $file->status_code < 300))) + { + $this->error = $file->error; + return !empty($this->data); + } + + if (!$this->force_feed) + { + // Check if the supplied URL is a feed, if it isn't, look for it. + $locate = $this->registry->create('Locator', array(&$file, $this->timeout, $this->useragent, $this->max_checked_feeds)); + + if (!$locate->is_feed($file)) + { + $copyStatusCode = $file->status_code; + $copyContentType = $file->headers['content-type']; + try + { + $microformats = false; + if (function_exists('Mf2\parse')) { + // Check for both h-feed and h-entry, as both a feed with no entries + // and a list of entries without an h-feed wrapper are both valid. + $position = 0; + while ($position = strpos($file->body, 'h-feed', $position)) + { + $start = $position < 200 ? 0 : $position - 200; + $check = substr($file->body, $start, 400); + if ($microformats = preg_match('/class="[^"]*h-feed/', $check)) + { + break; + } + $position += 7; + } + $position = 0; + while ($position = strpos($file->body, 'h-entry', $position)) + { + $start = $position < 200 ? 0 : $position - 200; + $check = substr($file->body, $start, 400); + if ($microformats = preg_match('/class="[^"]*h-entry/', $check)) + { + break; + } + $position += 7; + } + } + // Now also do feed discovery, but if an h-entry was found don't + // overwrite the current value of file. + $discovered = $locate->find($this->autodiscovery, + $this->all_discovered_feeds); + if ($microformats) + { + if ($hub = $locate->get_rel_link('hub')) + { + $self = $locate->get_rel_link('self'); + $this->store_links($file, $hub, $self); + } + // Push the current file onto all_discovered feeds so the user can + // be shown this as one of the options. + if (isset($this->all_discovered_feeds)) { + $this->all_discovered_feeds[] = $file; + } + } + else + { + if ($discovered) + { + $file = $discovered; + } + else + { + // We need to unset this so that if SimplePie::set_file() has + // been called that object is untouched + unset($file); + $this->error = "A feed could not be found at `$this->feed_url`; the status code is `$copyStatusCode` and content-type is `$copyContentType`"; + $this->registry->call('Misc', 'error', array($this->error, E_USER_NOTICE, __FILE__, __LINE__)); + return false; + } + } + } + catch (SimplePie_Exception $e) + { + // We need to unset this so that if SimplePie::set_file() has been called that object is untouched + unset($file); + // This is usually because DOMDocument doesn't exist + $this->error = $e->getMessage(); + $this->registry->call('Misc', 'error', array($this->error, E_USER_NOTICE, $e->getFile(), $e->getLine())); + return false; + } + if ($cache) + { + $this->data = array('url' => $this->feed_url, 'feed_url' => $file->url, 'build' => SIMPLEPIE_BUILD); + if (!$cache->save($this)) + { + trigger_error("$this->cache_location is not writeable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING); + } + $cache = $this->registry->call('Cache', 'get_handler', array($this->cache_location, call_user_func($this->cache_name_function, $file->url), 'spc')); + } + $this->feed_url = $file->url; + } + $locate = null; + } + + $this->raw_data = $file->body; + $this->permanent_url = $file->permanent_url; + $headers = $file->headers; + $sniffer = $this->registry->create('Content_Type_Sniffer', array(&$file)); + $sniffed = $sniffer->get_type(); + + return array($headers, $sniffed); + } + + /** + * Get the error message for the occured error + * + * @return string|array Error message, or array of messages for multifeeds + */ + public function error() + { + return $this->error; + } + + /** + * Get the raw XML + * + * This is the same as the old `$feed->enable_xml_dump(true)`, but returns + * the data instead of printing it. + * + * @return string|boolean Raw XML data, false if the cache is used + */ + public function get_raw_data() + { + return $this->raw_data; + } + + /** + * Get the character encoding used for output + * + * @since Preview Release + * @return string + */ + public function get_encoding() + { + return $this->sanitize->output_encoding; + } + + /** + * Send the content-type header with correct encoding + * + * This method ensures that the SimplePie-enabled page is being served with + * the correct {@link http://www.iana.org/assignments/media-types/ mime-type} + * and character encoding HTTP headers (character encoding determined by the + * {@see set_output_encoding} config option). + * + * This won't work properly if any content or whitespace has already been + * sent to the browser, because it relies on PHP's + * {@link http://php.net/header header()} function, and these are the + * circumstances under which the function works. + * + * Because it's setting these settings for the entire page (as is the nature + * of HTTP headers), this should only be used once per page (again, at the + * top). + * + * @param string $mime MIME type to serve the page as + */ + public function handle_content_type($mime = 'text/html') + { + if (!headers_sent()) + { + $header = "Content-type: $mime;"; + if ($this->get_encoding()) + { + $header .= ' charset=' . $this->get_encoding(); + } + else + { + $header .= ' charset=UTF-8'; + } + header($header); + } + } + + /** + * Get the type of the feed + * + * This returns a SIMPLEPIE_TYPE_* constant, which can be tested against + * using {@link http://php.net/language.operators.bitwise bitwise operators} + * + * @since 0.8 (usage changed to using constants in 1.0) + * @see SIMPLEPIE_TYPE_NONE Unknown. + * @see SIMPLEPIE_TYPE_RSS_090 RSS 0.90. + * @see SIMPLEPIE_TYPE_RSS_091_NETSCAPE RSS 0.91 (Netscape). + * @see SIMPLEPIE_TYPE_RSS_091_USERLAND RSS 0.91 (Userland). + * @see SIMPLEPIE_TYPE_RSS_091 RSS 0.91. + * @see SIMPLEPIE_TYPE_RSS_092 RSS 0.92. + * @see SIMPLEPIE_TYPE_RSS_093 RSS 0.93. + * @see SIMPLEPIE_TYPE_RSS_094 RSS 0.94. + * @see SIMPLEPIE_TYPE_RSS_10 RSS 1.0. + * @see SIMPLEPIE_TYPE_RSS_20 RSS 2.0.x. + * @see SIMPLEPIE_TYPE_RSS_RDF RDF-based RSS. + * @see SIMPLEPIE_TYPE_RSS_SYNDICATION Non-RDF-based RSS (truly intended as syndication format). + * @see SIMPLEPIE_TYPE_RSS_ALL Any version of RSS. + * @see SIMPLEPIE_TYPE_ATOM_03 Atom 0.3. + * @see SIMPLEPIE_TYPE_ATOM_10 Atom 1.0. + * @see SIMPLEPIE_TYPE_ATOM_ALL Any version of Atom. + * @see SIMPLEPIE_TYPE_ALL Any known/supported feed type. + * @return int SIMPLEPIE_TYPE_* constant + */ + public function get_type() + { + if (!isset($this->data['type'])) + { + $this->data['type'] = SIMPLEPIE_TYPE_ALL; + if (isset($this->data['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['feed'])) + { + $this->data['type'] &= SIMPLEPIE_TYPE_ATOM_10; + } + elseif (isset($this->data['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['feed'])) + { + $this->data['type'] &= SIMPLEPIE_TYPE_ATOM_03; + } + elseif (isset($this->data['child'][SIMPLEPIE_NAMESPACE_RDF]['RDF'])) + { + if (isset($this->data['child'][SIMPLEPIE_NAMESPACE_RDF]['RDF'][0]['child'][SIMPLEPIE_NAMESPACE_RSS_10]['channel']) + || isset($this->data['child'][SIMPLEPIE_NAMESPACE_RDF]['RDF'][0]['child'][SIMPLEPIE_NAMESPACE_RSS_10]['image']) + || isset($this->data['child'][SIMPLEPIE_NAMESPACE_RDF]['RDF'][0]['child'][SIMPLEPIE_NAMESPACE_RSS_10]['item']) + || isset($this->data['child'][SIMPLEPIE_NAMESPACE_RDF]['RDF'][0]['child'][SIMPLEPIE_NAMESPACE_RSS_10]['textinput'])) + { + $this->data['type'] &= SIMPLEPIE_TYPE_RSS_10; + } + if (isset($this->data['child'][SIMPLEPIE_NAMESPACE_RDF]['RDF'][0]['child'][SIMPLEPIE_NAMESPACE_RSS_090]['channel']) + || isset($this->data['child'][SIMPLEPIE_NAMESPACE_RDF]['RDF'][0]['child'][SIMPLEPIE_NAMESPACE_RSS_090]['image']) + || isset($this->data['child'][SIMPLEPIE_NAMESPACE_RDF]['RDF'][0]['child'][SIMPLEPIE_NAMESPACE_RSS_090]['item']) + || isset($this->data['child'][SIMPLEPIE_NAMESPACE_RDF]['RDF'][0]['child'][SIMPLEPIE_NAMESPACE_RSS_090]['textinput'])) + { + $this->data['type'] &= SIMPLEPIE_TYPE_RSS_090; + } + } + elseif (isset($this->data['child'][SIMPLEPIE_NAMESPACE_RSS_20]['rss'])) + { + $this->data['type'] &= SIMPLEPIE_TYPE_RSS_ALL; + if (isset($this->data['child'][SIMPLEPIE_NAMESPACE_RSS_20]['rss'][0]['attribs']['']['version'])) + { + switch (trim($this->data['child'][SIMPLEPIE_NAMESPACE_RSS_20]['rss'][0]['attribs']['']['version'])) + { + case '0.91': + $this->data['type'] &= SIMPLEPIE_TYPE_RSS_091; + if (isset($this->data['child'][SIMPLEPIE_NAMESPACE_RSS_20]['rss'][0]['child'][SIMPLEPIE_NAMESPACE_RSS_20]['skiphours']['hour'][0]['data'])) + { + switch (trim($this->data['child'][SIMPLEPIE_NAMESPACE_RSS_20]['rss'][0]['child'][SIMPLEPIE_NAMESPACE_RSS_20]['skiphours']['hour'][0]['data'])) + { + case '0': + $this->data['type'] &= SIMPLEPIE_TYPE_RSS_091_NETSCAPE; + break; + + case '24': + $this->data['type'] &= SIMPLEPIE_TYPE_RSS_091_USERLAND; + break; + } + } + break; + + case '0.92': + $this->data['type'] &= SIMPLEPIE_TYPE_RSS_092; + break; + + case '0.93': + $this->data['type'] &= SIMPLEPIE_TYPE_RSS_093; + break; + + case '0.94': + $this->data['type'] &= SIMPLEPIE_TYPE_RSS_094; + break; + + case '2.0': + $this->data['type'] &= SIMPLEPIE_TYPE_RSS_20; + break; + } + } + } + else + { + $this->data['type'] = SIMPLEPIE_TYPE_NONE; + } + } + return $this->data['type']; + } + + /** + * Get the URL for the feed + * + * When the 'permanent' mode is enabled, returns the original feed URL, + * except in the case of an `HTTP 301 Moved Permanently` status response, + * in which case the location of the first redirection is returned. + * + * When the 'permanent' mode is disabled (default), + * may or may not be different from the URL passed to {@see set_feed_url()}, + * depending on whether auto-discovery was used. + * + * @since Preview Release (previously called `get_feed_url()` since SimplePie 0.8.) + * @todo Support <itunes:new-feed-url> + * @todo Also, |atom:link|@rel=self + * @param bool $permanent Permanent mode to return only the original URL or the first redirection + * iff it is a 301 redirection + * @return string|null + */ + public function subscribe_url($permanent = false) + { + if ($permanent) + { + if ($this->permanent_url !== null) + { + // sanitize encodes ampersands which are required when used in a url. + return str_replace('&', '&', + $this->sanitize($this->permanent_url, + SIMPLEPIE_CONSTRUCT_IRI)); + } + } + else + { + if ($this->feed_url !== null) + { + return str_replace('&', '&', + $this->sanitize($this->feed_url, + SIMPLEPIE_CONSTRUCT_IRI)); + } + } + return null; + } + + /** + * Get data for an feed-level element + * + * This method allows you to get access to ANY element/attribute that is a + * sub-element of the opening feed tag. + * + * The return value is an indexed array of elements matching the given + * namespace and tag name. Each element has `attribs`, `data` and `child` + * subkeys. For `attribs` and `child`, these contain namespace subkeys. + * `attribs` then has one level of associative name => value data (where + * `value` is a string) after the namespace. `child` has tag-indexed keys + * after the namespace, each member of which is an indexed array matching + * this same format. + * + * For example: + * <pre> + * // This is probably a bad example because we already support + * // <media:content> natively, but it shows you how to parse through + * // the nodes. + * $group = $item->get_item_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'group'); + * $content = $group[0]['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['content']; + * $file = $content[0]['attribs']['']['url']; + * echo $file; + * </pre> + * + * @since 1.0 + * @see http://simplepie.org/wiki/faq/supported_xml_namespaces + * @param string $namespace The URL of the XML namespace of the elements you're trying to access + * @param string $tag Tag name + * @return array + */ + public function get_feed_tags($namespace, $tag) + { + $type = $this->get_type(); + if ($type & SIMPLEPIE_TYPE_ATOM_10) + { + if (isset($this->data['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['feed'][0]['child'][$namespace][$tag])) + { + return $this->data['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['feed'][0]['child'][$namespace][$tag]; + } + } + if ($type & SIMPLEPIE_TYPE_ATOM_03) + { + if (isset($this->data['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['feed'][0]['child'][$namespace][$tag])) + { + return $this->data['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['feed'][0]['child'][$namespace][$tag]; + } + } + if ($type & SIMPLEPIE_TYPE_RSS_RDF) + { + if (isset($this->data['child'][SIMPLEPIE_NAMESPACE_RDF]['RDF'][0]['child'][$namespace][$tag])) + { + return $this->data['child'][SIMPLEPIE_NAMESPACE_RDF]['RDF'][0]['child'][$namespace][$tag]; + } + } + if ($type & SIMPLEPIE_TYPE_RSS_SYNDICATION) + { + if (isset($this->data['child'][SIMPLEPIE_NAMESPACE_RSS_20]['rss'][0]['child'][$namespace][$tag])) + { + return $this->data['child'][SIMPLEPIE_NAMESPACE_RSS_20]['rss'][0]['child'][$namespace][$tag]; + } + } + return null; + } + + /** + * Get data for an channel-level element + * + * This method allows you to get access to ANY element/attribute in the + * channel/header section of the feed. + * + * See {@see SimplePie::get_feed_tags()} for a description of the return value + * + * @since 1.0 + * @see http://simplepie.org/wiki/faq/supported_xml_namespaces + * @param string $namespace The URL of the XML namespace of the elements you're trying to access + * @param string $tag Tag name + * @return array + */ + public function get_channel_tags($namespace, $tag) + { + $type = $this->get_type(); + if ($type & SIMPLEPIE_TYPE_ATOM_ALL) + { + if ($return = $this->get_feed_tags($namespace, $tag)) + { + return $return; + } + } + if ($type & SIMPLEPIE_TYPE_RSS_10) + { + if ($channel = $this->get_feed_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'channel')) + { + if (isset($channel[0]['child'][$namespace][$tag])) + { + return $channel[0]['child'][$namespace][$tag]; + } + } + } + if ($type & SIMPLEPIE_TYPE_RSS_090) + { + if ($channel = $this->get_feed_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'channel')) + { + if (isset($channel[0]['child'][$namespace][$tag])) + { + return $channel[0]['child'][$namespace][$tag]; + } + } + } + if ($type & SIMPLEPIE_TYPE_RSS_SYNDICATION) + { + if ($channel = $this->get_feed_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'channel')) + { + if (isset($channel[0]['child'][$namespace][$tag])) + { + return $channel[0]['child'][$namespace][$tag]; + } + } + } + return null; + } + + /** + * Get data for an channel-level element + * + * This method allows you to get access to ANY element/attribute in the + * image/logo section of the feed. + * + * See {@see SimplePie::get_feed_tags()} for a description of the return value + * + * @since 1.0 + * @see http://simplepie.org/wiki/faq/supported_xml_namespaces + * @param string $namespace The URL of the XML namespace of the elements you're trying to access + * @param string $tag Tag name + * @return array + */ + public function get_image_tags($namespace, $tag) + { + $type = $this->get_type(); + if ($type & SIMPLEPIE_TYPE_RSS_10) + { + if ($image = $this->get_feed_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'image')) + { + if (isset($image[0]['child'][$namespace][$tag])) + { + return $image[0]['child'][$namespace][$tag]; + } + } + } + if ($type & SIMPLEPIE_TYPE_RSS_090) + { + if ($image = $this->get_feed_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'image')) + { + if (isset($image[0]['child'][$namespace][$tag])) + { + return $image[0]['child'][$namespace][$tag]; + } + } + } + if ($type & SIMPLEPIE_TYPE_RSS_SYNDICATION) + { + if ($image = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'image')) + { + if (isset($image[0]['child'][$namespace][$tag])) + { + return $image[0]['child'][$namespace][$tag]; + } + } + } + return null; + } + + /** + * Get the base URL value from the feed + * + * Uses `<xml:base>` if available, otherwise uses the first link in the + * feed, or failing that, the URL of the feed itself. + * + * @see get_link + * @see subscribe_url + * + * @param array $element + * @return string + */ + public function get_base($element = array()) + { + if (!($this->get_type() & SIMPLEPIE_TYPE_RSS_SYNDICATION) && !empty($element['xml_base_explicit']) && isset($element['xml_base'])) + { + return $element['xml_base']; + } + elseif ($this->get_link() !== null) + { + return $this->get_link(); + } + else + { + return $this->subscribe_url(); + } + } + + /** + * Sanitize feed data + * + * @access private + * @see SimplePie_Sanitize::sanitize() + * @param string $data Data to sanitize + * @param int $type One of the SIMPLEPIE_CONSTRUCT_* constants + * @param string $base Base URL to resolve URLs against + * @return string Sanitized data + */ + public function sanitize($data, $type, $base = '') + { + try + { + return $this->sanitize->sanitize($data, $type, $base); + } + catch (SimplePie_Exception $e) + { + if (!$this->enable_exceptions) + { + $this->error = $e->getMessage(); + $this->registry->call('Misc', 'error', array($this->error, E_USER_WARNING, $e->getFile(), $e->getLine())); + return ''; + } + + throw $e; + } + } + + /** + * Get the title of the feed + * + * Uses `<atom:title>`, `<title>` or `<dc:title>` + * + * @since 1.0 (previously called `get_feed_title` since 0.8) + * @return string|null + */ + public function get_title() + { + if ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'title')) + { + return $this->sanitize($return[0]['data'], $this->registry->call('Misc', 'atom_10_construct_type', array($return[0]['attribs'])), $this->get_base($return[0])); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'title')) + { + return $this->sanitize($return[0]['data'], $this->registry->call('Misc', 'atom_03_construct_type', array($return[0]['attribs'])), $this->get_base($return[0])); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'title')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'title')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'title')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_DC_11, 'title')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_DC_10, 'title')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + return null; + } + } + + /** + * Get a category for the feed + * + * @since Unknown + * @param int $key The category that you want to return. Remember that arrays begin with 0, not 1 + * @return SimplePie_Category|null + */ + public function get_category($key = 0) + { + $categories = $this->get_categories(); + if (isset($categories[$key])) + { + return $categories[$key]; + } + else + { + return null; + } + } + + /** + * Get all categories for the feed + * + * Uses `<atom:category>`, `<category>` or `<dc:subject>` + * + * @since Unknown + * @return array|null List of {@see SimplePie_Category} objects + */ + public function get_categories() + { + $categories = array(); + + foreach ((array) $this->get_channel_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'category') as $category) + { + $term = null; + $scheme = null; + $label = null; + if (isset($category['attribs']['']['term'])) + { + $term = $this->sanitize($category['attribs']['']['term'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['scheme'])) + { + $scheme = $this->sanitize($category['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['label'])) + { + $label = $this->sanitize($category['attribs']['']['label'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $categories[] = $this->registry->create('Category', array($term, $scheme, $label)); + } + foreach ((array) $this->get_channel_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'category') as $category) + { + // This is really the label, but keep this as the term also for BC. + // Label will also work on retrieving because that falls back to term. + $term = $this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_TEXT); + if (isset($category['attribs']['']['domain'])) + { + $scheme = $this->sanitize($category['attribs']['']['domain'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $scheme = null; + } + $categories[] = $this->registry->create('Category', array($term, $scheme, null)); + } + foreach ((array) $this->get_channel_tags(SIMPLEPIE_NAMESPACE_DC_11, 'subject') as $category) + { + $categories[] = $this->registry->create('Category', array($this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_TEXT), null, null)); + } + foreach ((array) $this->get_channel_tags(SIMPLEPIE_NAMESPACE_DC_10, 'subject') as $category) + { + $categories[] = $this->registry->create('Category', array($this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_TEXT), null, null)); + } + + if (!empty($categories)) + { + return array_unique($categories); + } + else + { + return null; + } + } + + /** + * Get an author for the feed + * + * @since 1.1 + * @param int $key The author that you want to return. Remember that arrays begin with 0, not 1 + * @return SimplePie_Author|null + */ + public function get_author($key = 0) + { + $authors = $this->get_authors(); + if (isset($authors[$key])) + { + return $authors[$key]; + } + else + { + return null; + } + } + + /** + * Get all authors for the feed + * + * Uses `<atom:author>`, `<author>`, `<dc:creator>` or `<itunes:author>` + * + * @since 1.1 + * @return array|null List of {@see SimplePie_Author} objects + */ + public function get_authors() + { + $authors = array(); + foreach ((array) $this->get_channel_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'author') as $author) + { + $name = null; + $uri = null; + $email = null; + if (isset($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data'])) + { + $name = $this->sanitize($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data'])) + { + $uri = $this->sanitize($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0])); + } + if (isset($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['email'][0]['data'])) + { + $email = $this->sanitize($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['email'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $uri !== null) + { + $authors[] = $this->registry->create('Author', array($name, $uri, $email)); + } + } + if ($author = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'author')) + { + $name = null; + $url = null; + $email = null; + if (isset($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['name'][0]['data'])) + { + $name = $this->sanitize($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['name'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0]['data'])) + { + $url = $this->sanitize($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0])); + } + if (isset($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['email'][0]['data'])) + { + $email = $this->sanitize($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['email'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $url !== null) + { + $authors[] = $this->registry->create('Author', array($name, $url, $email)); + } + } + foreach ((array) $this->get_channel_tags(SIMPLEPIE_NAMESPACE_DC_11, 'creator') as $author) + { + $authors[] = $this->registry->create('Author', array($this->sanitize($author['data'], SIMPLEPIE_CONSTRUCT_TEXT), null, null)); + } + foreach ((array) $this->get_channel_tags(SIMPLEPIE_NAMESPACE_DC_10, 'creator') as $author) + { + $authors[] = $this->registry->create('Author', array($this->sanitize($author['data'], SIMPLEPIE_CONSTRUCT_TEXT), null, null)); + } + foreach ((array) $this->get_channel_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'author') as $author) + { + $authors[] = $this->registry->create('Author', array($this->sanitize($author['data'], SIMPLEPIE_CONSTRUCT_TEXT), null, null)); + } + + if (!empty($authors)) + { + return array_unique($authors); + } + else + { + return null; + } + } + + /** + * Get a contributor for the feed + * + * @since 1.1 + * @param int $key The contrbutor that you want to return. Remember that arrays begin with 0, not 1 + * @return SimplePie_Author|null + */ + public function get_contributor($key = 0) + { + $contributors = $this->get_contributors(); + if (isset($contributors[$key])) + { + return $contributors[$key]; + } + else + { + return null; + } + } + + /** + * Get all contributors for the feed + * + * Uses `<atom:contributor>` + * + * @since 1.1 + * @return array|null List of {@see SimplePie_Author} objects + */ + public function get_contributors() + { + $contributors = array(); + foreach ((array) $this->get_channel_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'contributor') as $contributor) + { + $name = null; + $uri = null; + $email = null; + if (isset($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data'])) + { + $name = $this->sanitize($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data'])) + { + $uri = $this->sanitize($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0])); + } + if (isset($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['email'][0]['data'])) + { + $email = $this->sanitize($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['email'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $uri !== null) + { + $contributors[] = $this->registry->create('Author', array($name, $uri, $email)); + } + } + foreach ((array) $this->get_channel_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'contributor') as $contributor) + { + $name = null; + $url = null; + $email = null; + if (isset($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['name'][0]['data'])) + { + $name = $this->sanitize($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['name'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0]['data'])) + { + $url = $this->sanitize($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0])); + } + if (isset($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['email'][0]['data'])) + { + $email = $this->sanitize($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['email'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $url !== null) + { + $contributors[] = $this->registry->create('Author', array($name, $url, $email)); + } + } + + if (!empty($contributors)) + { + return array_unique($contributors); + } + else + { + return null; + } + } + + /** + * Get a single link for the feed + * + * @since 1.0 (previously called `get_feed_link` since Preview Release, `get_feed_permalink()` since 0.8) + * @param int $key The link that you want to return. Remember that arrays begin with 0, not 1 + * @param string $rel The relationship of the link to return + * @return string|null Link URL + */ + public function get_link($key = 0, $rel = 'alternate') + { + $links = $this->get_links($rel); + if (isset($links[$key])) + { + return $links[$key]; + } + else + { + return null; + } + } + + /** + * Get the permalink for the item + * + * Returns the first link available with a relationship of "alternate". + * Identical to {@see get_link()} with key 0 + * + * @see get_link + * @since 1.0 (previously called `get_feed_link` since Preview Release, `get_feed_permalink()` since 0.8) + * @internal Added for parity between the parent-level and the item/entry-level. + * @return string|null Link URL + */ + public function get_permalink() + { + return $this->get_link(0); + } + + /** + * Get all links for the feed + * + * Uses `<atom:link>` or `<link>` + * + * @since Beta 2 + * @param string $rel The relationship of links to return + * @return array|null Links found for the feed (strings) + */ + public function get_links($rel = 'alternate') + { + if (!isset($this->data['links'])) + { + $this->data['links'] = array(); + if ($links = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'link')) + { + foreach ($links as $link) + { + if (isset($link['attribs']['']['href'])) + { + $link_rel = (isset($link['attribs']['']['rel'])) ? $link['attribs']['']['rel'] : 'alternate'; + $this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($link)); + } + } + } + if ($links = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'link')) + { + foreach ($links as $link) + { + if (isset($link['attribs']['']['href'])) + { + $link_rel = (isset($link['attribs']['']['rel'])) ? $link['attribs']['']['rel'] : 'alternate'; + $this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($link)); + + } + } + } + if ($links = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'link')) + { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($links[0])); + } + if ($links = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'link')) + { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($links[0])); + } + if ($links = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'link')) + { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($links[0])); + } + + $keys = array_keys($this->data['links']); + foreach ($keys as $key) + { + if ($this->registry->call('Misc', 'is_isegment_nz_nc', array($key))) + { + if (isset($this->data['links'][SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY . $key])) + { + $this->data['links'][SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY . $key] = array_merge($this->data['links'][$key], $this->data['links'][SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY . $key]); + $this->data['links'][$key] =& $this->data['links'][SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY . $key]; + } + else + { + $this->data['links'][SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY . $key] =& $this->data['links'][$key]; + } + } + elseif (substr($key, 0, 41) === SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY) + { + $this->data['links'][substr($key, 41)] =& $this->data['links'][$key]; + } + $this->data['links'][$key] = array_unique($this->data['links'][$key]); + } + } + + if (isset($this->data['links'][$rel])) + { + return $this->data['links'][$rel]; + } + else if (isset($this->data['headers']['link']) && + preg_match('/<([^>]+)>; rel='.preg_quote($rel).'/', + $this->data['headers']['link'], $match)) + { + return array($match[1]); + } + else + { + return null; + } + } + + public function get_all_discovered_feeds() + { + return $this->all_discovered_feeds; + } + + /** + * Get the content for the item + * + * Uses `<atom:subtitle>`, `<atom:tagline>`, `<description>`, + * `<dc:description>`, `<itunes:summary>` or `<itunes:subtitle>` + * + * @since 1.0 (previously called `get_feed_description()` since 0.8) + * @return string|null + */ + public function get_description() + { + if ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'subtitle')) + { + return $this->sanitize($return[0]['data'], $this->registry->call('Misc', 'atom_10_construct_type', array($return[0]['attribs'])), $this->get_base($return[0])); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'tagline')) + { + return $this->sanitize($return[0]['data'], $this->registry->call('Misc', 'atom_03_construct_type', array($return[0]['attribs'])), $this->get_base($return[0])); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'description')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'description')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'description')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_HTML, $this->get_base($return[0])); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_DC_11, 'description')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_DC_10, 'description')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'summary')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_HTML, $this->get_base($return[0])); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'subtitle')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_HTML, $this->get_base($return[0])); + } + else + { + return null; + } + } + + /** + * Get the copyright info for the feed + * + * Uses `<atom:rights>`, `<atom:copyright>` or `<dc:rights>` + * + * @since 1.0 (previously called `get_feed_copyright()` since 0.8) + * @return string|null + */ + public function get_copyright() + { + if ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'rights')) + { + return $this->sanitize($return[0]['data'], $this->registry->call('Misc', 'atom_10_construct_type', array($return[0]['attribs'])), $this->get_base($return[0])); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'copyright')) + { + return $this->sanitize($return[0]['data'], $this->registry->call('Misc', 'atom_03_construct_type', array($return[0]['attribs'])), $this->get_base($return[0])); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'copyright')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_DC_11, 'rights')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_DC_10, 'rights')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + return null; + } + } + + /** + * Get the language for the feed + * + * Uses `<language>`, `<dc:language>`, or @xml_lang + * + * @since 1.0 (previously called `get_feed_language()` since 0.8) + * @return string|null + */ + public function get_language() + { + if ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'language')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_DC_11, 'language')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_DC_10, 'language')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif (isset($this->data['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['feed'][0]['xml_lang'])) + { + return $this->sanitize($this->data['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['feed'][0]['xml_lang'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif (isset($this->data['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['feed'][0]['xml_lang'])) + { + return $this->sanitize($this->data['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['feed'][0]['xml_lang'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif (isset($this->data['child'][SIMPLEPIE_NAMESPACE_RDF]['RDF'][0]['xml_lang'])) + { + return $this->sanitize($this->data['child'][SIMPLEPIE_NAMESPACE_RDF]['RDF'][0]['xml_lang'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif (isset($this->data['headers']['content-language'])) + { + return $this->sanitize($this->data['headers']['content-language'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + return null; + } + } + + /** + * Get the latitude coordinates for the item + * + * Compatible with the W3C WGS84 Basic Geo and GeoRSS specifications + * + * Uses `<geo:lat>` or `<georss:point>` + * + * @since 1.0 + * @link http://www.w3.org/2003/01/geo/ W3C WGS84 Basic Geo + * @link http://www.georss.org/ GeoRSS + * @return string|null + */ + public function get_latitude() + { + + if ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_W3C_BASIC_GEO, 'lat')) + { + return (float) $return[0]['data']; + } + elseif (($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_GEORSS, 'point')) && preg_match('/^((?:-)?[0-9]+(?:\.[0-9]+)) ((?:-)?[0-9]+(?:\.[0-9]+))$/', trim($return[0]['data']), $match)) + { + return (float) $match[1]; + } + else + { + return null; + } + } + + /** + * Get the longitude coordinates for the feed + * + * Compatible with the W3C WGS84 Basic Geo and GeoRSS specifications + * + * Uses `<geo:long>`, `<geo:lon>` or `<georss:point>` + * + * @since 1.0 + * @link http://www.w3.org/2003/01/geo/ W3C WGS84 Basic Geo + * @link http://www.georss.org/ GeoRSS + * @return string|null + */ + public function get_longitude() + { + if ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_W3C_BASIC_GEO, 'long')) + { + return (float) $return[0]['data']; + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_W3C_BASIC_GEO, 'lon')) + { + return (float) $return[0]['data']; + } + elseif (($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_GEORSS, 'point')) && preg_match('/^((?:-)?[0-9]+(?:\.[0-9]+)) ((?:-)?[0-9]+(?:\.[0-9]+))$/', trim($return[0]['data']), $match)) + { + return (float) $match[2]; + } + else + { + return null; + } + } + + /** + * Get the feed logo's title + * + * RSS 0.9.0, 1.0 and 2.0 feeds are allowed to have a "feed logo" title. + * + * Uses `<image><title>` or `<image><dc:title>` + * + * @return string|null + */ + public function get_image_title() + { + if ($return = $this->get_image_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'title')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_image_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'title')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_image_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'title')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_image_tags(SIMPLEPIE_NAMESPACE_DC_11, 'title')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_image_tags(SIMPLEPIE_NAMESPACE_DC_10, 'title')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + return null; + } + } + + /** + * Get the feed logo's URL + * + * RSS 0.9.0, 2.0, Atom 1.0, and feeds with iTunes RSS tags are allowed to + * have a "feed logo" URL. This points directly to the image itself. + * + * Uses `<itunes:image>`, `<atom:logo>`, `<atom:icon>`, + * `<image><title>` or `<image><dc:title>` + * + * @return string|null + */ + public function get_image_url() + { + if ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'image')) + { + return $this->sanitize($return[0]['attribs']['']['href'], SIMPLEPIE_CONSTRUCT_IRI); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'logo')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($return[0])); + } + elseif ($return = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'icon')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($return[0])); + } + elseif ($return = $this->get_image_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'url')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($return[0])); + } + elseif ($return = $this->get_image_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'url')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($return[0])); + } + elseif ($return = $this->get_image_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'url')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($return[0])); + } + else + { + return null; + } + } + + + /** + * Get the feed logo's link + * + * RSS 0.9.0, 1.0 and 2.0 feeds are allowed to have a "feed logo" link. This + * points to a human-readable page that the image should link to. + * + * Uses `<itunes:image>`, `<atom:logo>`, `<atom:icon>`, + * `<image><title>` or `<image><dc:title>` + * + * @return string|null + */ + public function get_image_link() + { + if ($return = $this->get_image_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'link')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($return[0])); + } + elseif ($return = $this->get_image_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'link')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($return[0])); + } + elseif ($return = $this->get_image_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'link')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($return[0])); + } + else + { + return null; + } + } + + /** + * Get the feed logo's link + * + * RSS 2.0 feeds are allowed to have a "feed logo" width. + * + * Uses `<image><width>` or defaults to 88.0 if no width is specified and + * the feed is an RSS 2.0 feed. + * + * @return int|float|null + */ + public function get_image_width() + { + if ($return = $this->get_image_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'width')) + { + return round($return[0]['data']); + } + elseif ($this->get_type() & SIMPLEPIE_TYPE_RSS_SYNDICATION && $this->get_image_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'url')) + { + return 88.0; + } + else + { + return null; + } + } + + /** + * Get the feed logo's height + * + * RSS 2.0 feeds are allowed to have a "feed logo" height. + * + * Uses `<image><height>` or defaults to 31.0 if no height is specified and + * the feed is an RSS 2.0 feed. + * + * @return int|float|null + */ + public function get_image_height() + { + if ($return = $this->get_image_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'height')) + { + return round($return[0]['data']); + } + elseif ($this->get_type() & SIMPLEPIE_TYPE_RSS_SYNDICATION && $this->get_image_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'url')) + { + return 31.0; + } + else + { + return null; + } + } + + /** + * Get the number of items in the feed + * + * This is well-suited for {@link http://php.net/for for()} loops with + * {@see get_item()} + * + * @param int $max Maximum value to return. 0 for no limit + * @return int Number of items in the feed + */ + public function get_item_quantity($max = 0) + { + $max = (int) $max; + $qty = count($this->get_items()); + if ($max === 0) + { + return $qty; + } + else + { + return ($qty > $max) ? $max : $qty; + } + } + + /** + * Get a single item from the feed + * + * This is better suited for {@link http://php.net/for for()} loops, whereas + * {@see get_items()} is better suited for + * {@link http://php.net/foreach foreach()} loops. + * + * @see get_item_quantity() + * @since Beta 2 + * @param int $key The item that you want to return. Remember that arrays begin with 0, not 1 + * @return SimplePie_Item|null + */ + public function get_item($key = 0) + { + $items = $this->get_items(); + if (isset($items[$key])) + { + return $items[$key]; + } + else + { + return null; + } + } + + /** + * Get all items from the feed + * + * This is better suited for {@link http://php.net/for for()} loops, whereas + * {@see get_items()} is better suited for + * {@link http://php.net/foreach foreach()} loops. + * + * @see get_item_quantity + * @since Beta 2 + * @param int $start Index to start at + * @param int $end Number of items to return. 0 for all items after `$start` + * @return SimplePie_Item[]|null List of {@see SimplePie_Item} objects + */ + public function get_items($start = 0, $end = 0) + { + if (!isset($this->data['items'])) + { + if (!empty($this->multifeed_objects)) + { + $this->data['items'] = SimplePie::merge_items($this->multifeed_objects, $start, $end, $this->item_limit); + if (empty($this->data['items'])) + { + return array(); + } + return $this->data['items']; + } + $this->data['items'] = array(); + if ($items = $this->get_feed_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'entry')) + { + $keys = array_keys($items); + foreach ($keys as $key) + { + $this->data['items'][] = $this->registry->create('Item', array($this, $items[$key])); + } + } + if ($items = $this->get_feed_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'entry')) + { + $keys = array_keys($items); + foreach ($keys as $key) + { + $this->data['items'][] = $this->registry->create('Item', array($this, $items[$key])); + } + } + if ($items = $this->get_feed_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'item')) + { + $keys = array_keys($items); + foreach ($keys as $key) + { + $this->data['items'][] = $this->registry->create('Item', array($this, $items[$key])); + } + } + if ($items = $this->get_feed_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'item')) + { + $keys = array_keys($items); + foreach ($keys as $key) + { + $this->data['items'][] = $this->registry->create('Item', array($this, $items[$key])); + } + } + if ($items = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'item')) + { + $keys = array_keys($items); + foreach ($keys as $key) + { + $this->data['items'][] = $this->registry->create('Item', array($this, $items[$key])); + } + } + } + + if (empty($this->data['items'])) + { + return array(); + } + + if ($this->order_by_date) + { + if (!isset($this->data['ordered_items'])) + { + $this->data['ordered_items'] = $this->data['items']; + usort($this->data['ordered_items'], array(get_class($this), 'sort_items')); + } + $items = $this->data['ordered_items']; + } + else + { + $items = $this->data['items']; + } + // Slice the data as desired + if ($end === 0) + { + return array_slice($items, $start); + } + else + { + return array_slice($items, $start, $end); + } + } + + /** + * Set the favicon handler + * + * @deprecated Use your own favicon handling instead + */ + public function set_favicon_handler($page = false, $qs = 'i') + { + $level = defined('E_USER_DEPRECATED') ? E_USER_DEPRECATED : E_USER_WARNING; + trigger_error('Favicon handling has been removed, please use your own handling', $level); + return false; + } + + /** + * Get the favicon for the current feed + * + * @deprecated Use your own favicon handling instead + */ + public function get_favicon() + { + $level = defined('E_USER_DEPRECATED') ? E_USER_DEPRECATED : E_USER_WARNING; + trigger_error('Favicon handling has been removed, please use your own handling', $level); + + if (($url = $this->get_link()) !== null) + { + return 'http://g.etfv.co/' . urlencode($url); + } + + return false; + } + + /** + * Magic method handler + * + * @param string $method Method name + * @param array $args Arguments to the method + * @return mixed + */ + public function __call($method, $args) + { + if (strpos($method, 'subscribe_') === 0) + { + $level = defined('E_USER_DEPRECATED') ? E_USER_DEPRECATED : E_USER_WARNING; + trigger_error('subscribe_*() has been deprecated, implement the callback yourself', $level); + return ''; + } + if ($method === 'enable_xml_dump') + { + $level = defined('E_USER_DEPRECATED') ? E_USER_DEPRECATED : E_USER_WARNING; + trigger_error('enable_xml_dump() has been deprecated, use get_raw_data() instead', $level); + return false; + } + + $class = get_class($this); + $trace = debug_backtrace(); + $file = $trace[0]['file']; + $line = $trace[0]['line']; + trigger_error("Call to undefined method $class::$method() in $file on line $line", E_USER_ERROR); + } + + /** + * Sorting callback for items + * + * @access private + * @param SimplePie $a + * @param SimplePie $b + * @return boolean + */ + public static function sort_items($a, $b) + { + $a_date = $a->get_date('U'); + $b_date = $b->get_date('U'); + if ($a_date && $b_date) { + return $a_date > $b_date ? -1 : 1; + } + // Sort items without dates to the top. + if ($a_date) { + return 1; + } + if ($b_date) { + return -1; + } + return 0; + } + + /** + * Merge items from several feeds into one + * + * If you're merging multiple feeds together, they need to all have dates + * for the items or else SimplePie will refuse to sort them. + * + * @link http://simplepie.org/wiki/tutorial/sort_multiple_feeds_by_time_and_date#if_feeds_require_separate_per-feed_settings + * @param array $urls List of SimplePie feed objects to merge + * @param int $start Starting item + * @param int $end Number of items to return + * @param int $limit Maximum number of items per feed + * @return array + */ + public static function merge_items($urls, $start = 0, $end = 0, $limit = 0) + { + if (is_array($urls) && sizeof($urls) > 0) + { + $items = array(); + foreach ($urls as $arg) + { + if ($arg instanceof SimplePie) + { + $items = array_merge($items, $arg->get_items(0, $limit)); + } + else + { + trigger_error('Arguments must be SimplePie objects', E_USER_WARNING); + } + } + + usort($items, array(get_class($urls[0]), 'sort_items')); + + if ($end === 0) + { + return array_slice($items, $start); + } + else + { + return array_slice($items, $start, $end); + } + } + else + { + trigger_error('Cannot merge zero SimplePie objects', E_USER_WARNING); + return array(); + } + } + + /** + * Store PubSubHubbub links as headers + * + * There is no way to find PuSH links in the body of a microformats feed, + * so they are added to the headers when found, to be used later by get_links. + * @param SimplePie_File $file + * @param string $hub + * @param string $self + */ + private function store_links(&$file, $hub, $self) { + if (isset($file->headers['link']['hub']) || + (isset($file->headers['link']) && + preg_match('/rel=hub/', $file->headers['link']))) + { + return; + } + + if ($hub) + { + if (isset($file->headers['link'])) + { + if ($file->headers['link'] !== '') + { + $file->headers['link'] = ', '; + } + } + else + { + $file->headers['link'] = ''; + } + $file->headers['link'] .= '<'.$hub.'>; rel=hub'; + if ($self) + { + $file->headers['link'] .= ', <'.$self.'>; rel=self'; + } + } + } +} diff --git a/vendor/simplepie/simplepie/library/SimplePie/Author.php b/vendor/simplepie/simplepie/library/SimplePie/Author.php new file mode 100644 index 000000000..e6768ff29 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Author.php @@ -0,0 +1,156 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Manages all author-related data + * + * Used by {@see SimplePie_Item::get_author()} and {@see SimplePie::get_authors()} + * + * This class can be overloaded with {@see SimplePie::set_author_class()} + * + * @package SimplePie + * @subpackage API + */ +class SimplePie_Author +{ + /** + * Author's name + * + * @var string + * @see get_name() + */ + var $name; + + /** + * Author's link + * + * @var string + * @see get_link() + */ + var $link; + + /** + * Author's email address + * + * @var string + * @see get_email() + */ + var $email; + + /** + * Constructor, used to input the data + * + * @param string $name + * @param string $link + * @param string $email + */ + public function __construct($name = null, $link = null, $email = null) + { + $this->name = $name; + $this->link = $link; + $this->email = $email; + } + + /** + * String-ified version + * + * @return string + */ + public function __toString() + { + // There is no $this->data here + return md5(serialize($this)); + } + + /** + * Author's name + * + * @return string|null + */ + public function get_name() + { + if ($this->name !== null) + { + return $this->name; + } + else + { + return null; + } + } + + /** + * Author's link + * + * @return string|null + */ + public function get_link() + { + if ($this->link !== null) + { + return $this->link; + } + else + { + return null; + } + } + + /** + * Author's email address + * + * @return string|null + */ + public function get_email() + { + if ($this->email !== null) + { + return $this->email; + } + else + { + return null; + } + } +} + diff --git a/vendor/simplepie/simplepie/library/SimplePie/Cache.php b/vendor/simplepie/simplepie/library/SimplePie/Cache.php new file mode 100644 index 000000000..d98cc6511 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Cache.php @@ -0,0 +1,134 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Used to create cache objects + * + * This class can be overloaded with {@see SimplePie::set_cache_class()}, + * although the preferred way is to create your own handler + * via {@see register()} + * + * @package SimplePie + * @subpackage Caching + */ +class SimplePie_Cache +{ + /** + * Cache handler classes + * + * These receive 3 parameters to their constructor, as documented in + * {@see register()} + * @var array + */ + protected static $handlers = array( + 'mysql' => 'SimplePie_Cache_MySQL', + 'memcache' => 'SimplePie_Cache_Memcache', + 'memcached' => 'SimplePie_Cache_Memcached', + 'redis' => 'SimplePie_Cache_Redis' + ); + + /** + * Don't call the constructor. Please. + */ + private function __construct() { } + + /** + * Create a new SimplePie_Cache object + * + * @param string $location URL location (scheme is used to determine handler) + * @param string $filename Unique identifier for cache object + * @param string $extension 'spi' or 'spc' + * @return SimplePie_Cache_Base Type of object depends on scheme of `$location` + */ + public static function get_handler($location, $filename, $extension) + { + $type = explode(':', $location, 2); + $type = $type[0]; + if (!empty(self::$handlers[$type])) + { + $class = self::$handlers[$type]; + return new $class($location, $filename, $extension); + } + + return new SimplePie_Cache_File($location, $filename, $extension); + } + + /** + * Create a new SimplePie_Cache object + * + * @deprecated Use {@see get_handler} instead + */ + public function create($location, $filename, $extension) + { + trigger_error('Cache::create() has been replaced with Cache::get_handler(). Switch to the registry system to use this.', E_USER_DEPRECATED); + return self::get_handler($location, $filename, $extension); + } + + /** + * Register a handler + * + * @param string $type DSN type to register for + * @param string $class Name of handler class. Must implement SimplePie_Cache_Base + */ + public static function register($type, $class) + { + self::$handlers[$type] = $class; + } + + /** + * Parse a URL into an array + * + * @param string $url + * @return array + */ + public static function parse_URL($url) + { + $params = parse_url($url); + $params['extras'] = array(); + if (isset($params['query'])) + { + parse_str($params['query'], $params['extras']); + } + return $params; + } +} diff --git a/vendor/simplepie/simplepie/library/SimplePie/Cache/Base.php b/vendor/simplepie/simplepie/library/SimplePie/Cache/Base.php new file mode 100644 index 000000000..333fb05cf --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Cache/Base.php @@ -0,0 +1,113 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Base for cache objects + * + * Classes to be used with {@see SimplePie_Cache::register()} are expected + * to implement this interface. + * + * @package SimplePie + * @subpackage Caching + */ +interface SimplePie_Cache_Base +{ + /** + * Feed cache type + * + * @var string + */ + const TYPE_FEED = 'spc'; + + /** + * Image cache type + * + * @var string + */ + const TYPE_IMAGE = 'spi'; + + /** + * Create a new cache object + * + * @param string $location Location string (from SimplePie::$cache_location) + * @param string $name Unique ID for the cache + * @param string $type Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data + */ + public function __construct($location, $name, $type); + + /** + * Save data to the cache + * + * @param array|SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property + * @return bool Successfulness + */ + public function save($data); + + /** + * Retrieve the data saved to the cache + * + * @return array Data for SimplePie::$data + */ + public function load(); + + /** + * Retrieve the last modified time for the cache + * + * @return int Timestamp + */ + public function mtime(); + + /** + * Set the last modified time to the current time + * + * @return bool Success status + */ + public function touch(); + + /** + * Remove the cache + * + * @return bool Success status + */ + public function unlink(); +} diff --git a/vendor/simplepie/simplepie/library/SimplePie/Cache/DB.php b/vendor/simplepie/simplepie/library/SimplePie/Cache/DB.php new file mode 100644 index 000000000..7e8f77532 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Cache/DB.php @@ -0,0 +1,136 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Base class for database-based caches + * + * @package SimplePie + * @subpackage Caching + */ +abstract class SimplePie_Cache_DB implements SimplePie_Cache_Base +{ + /** + * Helper for database conversion + * + * Converts a given {@see SimplePie} object into data to be stored + * + * @param SimplePie $data + * @return array First item is the serialized data for storage, second item is the unique ID for this item + */ + protected static function prepare_simplepie_object_for_cache($data) + { + $items = $data->get_items(); + $items_by_id = array(); + + if (!empty($items)) + { + foreach ($items as $item) + { + $items_by_id[$item->get_id()] = $item; + } + + if (count($items_by_id) !== count($items)) + { + $items_by_id = array(); + foreach ($items as $item) + { + $items_by_id[$item->get_id(true)] = $item; + } + } + + if (isset($data->data['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['feed'][0])) + { + $channel =& $data->data['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['feed'][0]; + } + elseif (isset($data->data['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['feed'][0])) + { + $channel =& $data->data['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['feed'][0]; + } + elseif (isset($data->data['child'][SIMPLEPIE_NAMESPACE_RDF]['RDF'][0])) + { + $channel =& $data->data['child'][SIMPLEPIE_NAMESPACE_RDF]['RDF'][0]; + } + elseif (isset($data->data['child'][SIMPLEPIE_NAMESPACE_RSS_20]['rss'][0]['child'][SIMPLEPIE_NAMESPACE_RSS_20]['channel'][0])) + { + $channel =& $data->data['child'][SIMPLEPIE_NAMESPACE_RSS_20]['rss'][0]['child'][SIMPLEPIE_NAMESPACE_RSS_20]['channel'][0]; + } + else + { + $channel = null; + } + + if ($channel !== null) + { + if (isset($channel['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['entry'])) + { + unset($channel['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['entry']); + } + if (isset($channel['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['entry'])) + { + unset($channel['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['entry']); + } + if (isset($channel['child'][SIMPLEPIE_NAMESPACE_RSS_10]['item'])) + { + unset($channel['child'][SIMPLEPIE_NAMESPACE_RSS_10]['item']); + } + if (isset($channel['child'][SIMPLEPIE_NAMESPACE_RSS_090]['item'])) + { + unset($channel['child'][SIMPLEPIE_NAMESPACE_RSS_090]['item']); + } + if (isset($channel['child'][SIMPLEPIE_NAMESPACE_RSS_20]['item'])) + { + unset($channel['child'][SIMPLEPIE_NAMESPACE_RSS_20]['item']); + } + } + if (isset($data->data['items'])) + { + unset($data->data['items']); + } + if (isset($data->data['ordered_items'])) + { + unset($data->data['ordered_items']); + } + } + return array(serialize($data->data), $items_by_id); + } +} diff --git a/vendor/simplepie/simplepie/library/SimplePie/Cache/File.php b/vendor/simplepie/simplepie/library/SimplePie/Cache/File.php new file mode 100644 index 000000000..6ba6c5f6e --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Cache/File.php @@ -0,0 +1,164 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Caches data to the filesystem + * + * @package SimplePie + * @subpackage Caching + */ +class SimplePie_Cache_File implements SimplePie_Cache_Base +{ + /** + * Location string + * + * @see SimplePie::$cache_location + * @var string + */ + protected $location; + + /** + * Filename + * + * @var string + */ + protected $filename; + + /** + * File extension + * + * @var string + */ + protected $extension; + + /** + * File path + * + * @var string + */ + protected $name; + + /** + * Create a new cache object + * + * @param string $location Location string (from SimplePie::$cache_location) + * @param string $name Unique ID for the cache + * @param string $type Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data + */ + public function __construct($location, $name, $type) + { + $this->location = $location; + $this->filename = $name; + $this->extension = $type; + $this->name = "$this->location/$this->filename.$this->extension"; + } + + /** + * Save data to the cache + * + * @param array|SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property + * @return bool Successfulness + */ + public function save($data) + { + if (file_exists($this->name) && is_writeable($this->name) || file_exists($this->location) && is_writeable($this->location)) + { + if ($data instanceof SimplePie) + { + $data = $data->data; + } + + $data = serialize($data); + return (bool) file_put_contents($this->name, $data); + } + return false; + } + + /** + * Retrieve the data saved to the cache + * + * @return array Data for SimplePie::$data + */ + public function load() + { + if (file_exists($this->name) && is_readable($this->name)) + { + return unserialize(file_get_contents($this->name)); + } + return false; + } + + /** + * Retrieve the last modified time for the cache + * + * @return int Timestamp + */ + public function mtime() + { + return @filemtime($this->name); + } + + /** + * Set the last modified time to the current time + * + * @return bool Success status + */ + public function touch() + { + return @touch($this->name); + } + + /** + * Remove the cache + * + * @return bool Success status + */ + public function unlink() + { + if (file_exists($this->name)) + { + return unlink($this->name); + } + return false; + } +} diff --git a/vendor/simplepie/simplepie/library/SimplePie/Cache/Memcache.php b/vendor/simplepie/simplepie/library/SimplePie/Cache/Memcache.php new file mode 100644 index 000000000..5190eef93 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Cache/Memcache.php @@ -0,0 +1,180 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Caches data to memcache + * + * Registered for URLs with the "memcache" protocol + * + * For example, `memcache://localhost:11211/?timeout=3600&prefix=sp_` will + * connect to memcache on `localhost` on port 11211. All tables will be + * prefixed with `sp_` and data will expire after 3600 seconds + * + * @package SimplePie + * @subpackage Caching + * @uses Memcache + */ +class SimplePie_Cache_Memcache implements SimplePie_Cache_Base +{ + /** + * Memcache instance + * + * @var Memcache + */ + protected $cache; + + /** + * Options + * + * @var array + */ + protected $options; + + /** + * Cache name + * + * @var string + */ + protected $name; + + /** + * Create a new cache object + * + * @param string $location Location string (from SimplePie::$cache_location) + * @param string $name Unique ID for the cache + * @param string $type Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data + */ + public function __construct($location, $name, $type) + { + $this->options = array( + 'host' => '127.0.0.1', + 'port' => 11211, + 'extras' => array( + 'timeout' => 3600, // one hour + 'prefix' => 'simplepie_', + ), + ); + $this->options = SimplePie_Misc::array_merge_recursive($this->options, SimplePie_Cache::parse_URL($location)); + + $this->name = $this->options['extras']['prefix'] . md5("$name:$type"); + + $this->cache = new Memcache(); + $this->cache->addServer($this->options['host'], (int) $this->options['port']); + } + + /** + * Save data to the cache + * + * @param array|SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property + * @return bool Successfulness + */ + public function save($data) + { + if ($data instanceof SimplePie) + { + $data = $data->data; + } + return $this->cache->set($this->name, serialize($data), MEMCACHE_COMPRESSED, (int) $this->options['extras']['timeout']); + } + + /** + * Retrieve the data saved to the cache + * + * @return array Data for SimplePie::$data + */ + public function load() + { + $data = $this->cache->get($this->name); + + if ($data !== false) + { + return unserialize($data); + } + return false; + } + + /** + * Retrieve the last modified time for the cache + * + * @return int Timestamp + */ + public function mtime() + { + $data = $this->cache->get($this->name); + + if ($data !== false) + { + // essentially ignore the mtime because Memcache expires on its own + return time(); + } + + return false; + } + + /** + * Set the last modified time to the current time + * + * @return bool Success status + */ + public function touch() + { + $data = $this->cache->get($this->name); + + if ($data !== false) + { + return $this->cache->set($this->name, $data, MEMCACHE_COMPRESSED, (int) $this->options['extras']['timeout']); + } + + return false; + } + + /** + * Remove the cache + * + * @return bool Success status + */ + public function unlink() + { + return $this->cache->delete($this->name, 0); + } +} diff --git a/vendor/simplepie/simplepie/library/SimplePie/Cache/Memcached.php b/vendor/simplepie/simplepie/library/SimplePie/Cache/Memcached.php new file mode 100755 index 000000000..1f73b3890 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Cache/Memcached.php @@ -0,0 +1,166 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Caches data to memcached + * + * Registered for URLs with the "memcached" protocol + * + * For example, `memcached://localhost:11211/?timeout=3600&prefix=sp_` will + * connect to memcached on `localhost` on port 11211. All tables will be + * prefixed with `sp_` and data will expire after 3600 seconds + * + * @package SimplePie + * @subpackage Caching + * @author Paul L. McNeely + * @uses Memcached + */ +class SimplePie_Cache_Memcached implements SimplePie_Cache_Base +{ + /** + * Memcached instance + * @var Memcached + */ + protected $cache; + + /** + * Options + * @var array + */ + protected $options; + + /** + * Cache name + * @var string + */ + protected $name; + + /** + * Create a new cache object + * @param string $location Location string (from SimplePie::$cache_location) + * @param string $name Unique ID for the cache + * @param string $type Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data + */ + public function __construct($location, $name, $type) { + $this->options = array( + 'host' => '127.0.0.1', + 'port' => 11211, + 'extras' => array( + 'timeout' => 3600, // one hour + 'prefix' => 'simplepie_', + ), + ); + $this->options = SimplePie_Misc::array_merge_recursive($this->options, SimplePie_Cache::parse_URL($location)); + + $this->name = $this->options['extras']['prefix'] . md5("$name:$type"); + + $this->cache = new Memcached(); + $this->cache->addServer($this->options['host'], (int)$this->options['port']); + } + + /** + * Save data to the cache + * @param array|SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property + * @return bool Successfulness + */ + public function save($data) { + if ($data instanceof SimplePie) { + $data = $data->data; + } + + return $this->setData(serialize($data)); + } + + /** + * Retrieve the data saved to the cache + * @return array Data for SimplePie::$data + */ + public function load() { + $data = $this->cache->get($this->name); + + if ($data !== false) { + return unserialize($data); + } + return false; + } + + /** + * Retrieve the last modified time for the cache + * @return int Timestamp + */ + public function mtime() { + $data = $this->cache->get($this->name . '_mtime'); + return (int) $data; + } + + /** + * Set the last modified time to the current time + * @return bool Success status + */ + public function touch() { + $data = $this->cache->get($this->name); + return $this->setData($data); + } + + /** + * Remove the cache + * @return bool Success status + */ + public function unlink() { + return $this->cache->delete($this->name, 0); + } + + /** + * Set the last modified time and data to Memcached + * @return bool Success status + */ + private function setData($data) { + + if ($data !== false) { + $this->cache->set($this->name . '_mtime', time(), (int)$this->options['extras']['timeout']); + return $this->cache->set($this->name, $data, (int)$this->options['extras']['timeout']); + } + + return false; + } +} diff --git a/vendor/simplepie/simplepie/library/SimplePie/Cache/MySQL.php b/vendor/simplepie/simplepie/library/SimplePie/Cache/MySQL.php new file mode 100644 index 000000000..8686b6c67 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Cache/MySQL.php @@ -0,0 +1,454 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Caches data to a MySQL database + * + * Registered for URLs with the "mysql" protocol + * + * For example, `mysql://root:password@localhost:3306/mydb?prefix=sp_` will + * connect to the `mydb` database on `localhost` on port 3306, with the user + * `root` and the password `password`. All tables will be prefixed with `sp_` + * + * @package SimplePie + * @subpackage Caching + */ +class SimplePie_Cache_MySQL extends SimplePie_Cache_DB +{ + /** + * PDO instance + * + * @var PDO + */ + protected $mysql; + + /** + * Options + * + * @var array + */ + protected $options; + + /** + * Cache ID + * + * @var string + */ + protected $id; + + /** + * Create a new cache object + * + * @param string $location Location string (from SimplePie::$cache_location) + * @param string $name Unique ID for the cache + * @param string $type Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data + */ + public function __construct($location, $name, $type) + { + $this->options = array( + 'user' => null, + 'pass' => null, + 'host' => '127.0.0.1', + 'port' => '3306', + 'path' => '', + 'extras' => array( + 'prefix' => '', + 'cache_purge_time' => 2592000 + ), + ); + + $this->options = SimplePie_Misc::array_merge_recursive($this->options, SimplePie_Cache::parse_URL($location)); + + // Path is prefixed with a "/" + $this->options['dbname'] = substr($this->options['path'], 1); + + try + { + $this->mysql = new PDO("mysql:dbname={$this->options['dbname']};host={$this->options['host']};port={$this->options['port']}", $this->options['user'], $this->options['pass'], array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8')); + } + catch (PDOException $e) + { + $this->mysql = null; + return; + } + + $this->id = $name . $type; + + if (!$query = $this->mysql->query('SHOW TABLES')) + { + $this->mysql = null; + return; + } + + $db = array(); + while ($row = $query->fetchColumn()) + { + $db[] = $row; + } + + if (!in_array($this->options['extras']['prefix'] . 'cache_data', $db)) + { + $query = $this->mysql->exec('CREATE TABLE `' . $this->options['extras']['prefix'] . 'cache_data` (`id` TEXT CHARACTER SET utf8 NOT NULL, `items` SMALLINT NOT NULL DEFAULT 0, `data` BLOB NOT NULL, `mtime` INT UNSIGNED NOT NULL, UNIQUE (`id`(125)))'); + if ($query === false) + { + trigger_error("Can't create " . $this->options['extras']['prefix'] . "cache_data table, check permissions", E_USER_WARNING); + $this->mysql = null; + return; + } + } + + if (!in_array($this->options['extras']['prefix'] . 'items', $db)) + { + $query = $this->mysql->exec('CREATE TABLE `' . $this->options['extras']['prefix'] . 'items` (`feed_id` TEXT CHARACTER SET utf8 NOT NULL, `id` TEXT CHARACTER SET utf8 NOT NULL, `data` MEDIUMBLOB NOT NULL, `posted` INT UNSIGNED NOT NULL, INDEX `feed_id` (`feed_id`(125)))'); + if ($query === false) + { + trigger_error("Can't create " . $this->options['extras']['prefix'] . "items table, check permissions", E_USER_WARNING); + $this->mysql = null; + return; + } + } + } + + /** + * Save data to the cache + * + * @param array|SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property + * @return bool Successfulness + */ + public function save($data) + { + if ($this->mysql === null) + { + return false; + } + + $query = $this->mysql->prepare('DELETE i, cd FROM `' . $this->options['extras']['prefix'] . 'cache_data` cd, ' . + '`' . $this->options['extras']['prefix'] . 'items` i ' . + 'WHERE cd.id = i.feed_id ' . + 'AND cd.mtime < (unix_timestamp() - :purge_time)'); + $query->bindValue(':purge_time', $this->options['extras']['cache_purge_time']); + + if (!$query->execute()) + { + return false; + } + + if ($data instanceof SimplePie) + { + $data = clone $data; + + $prepared = self::prepare_simplepie_object_for_cache($data); + + $query = $this->mysql->prepare('SELECT COUNT(*) FROM `' . $this->options['extras']['prefix'] . 'cache_data` WHERE `id` = :feed'); + $query->bindValue(':feed', $this->id); + if ($query->execute()) + { + if ($query->fetchColumn() > 0) + { + $items = count($prepared[1]); + if ($items) + { + $sql = 'UPDATE `' . $this->options['extras']['prefix'] . 'cache_data` SET `items` = :items, `data` = :data, `mtime` = :time WHERE `id` = :feed'; + $query = $this->mysql->prepare($sql); + $query->bindValue(':items', $items); + } + else + { + $sql = 'UPDATE `' . $this->options['extras']['prefix'] . 'cache_data` SET `data` = :data, `mtime` = :time WHERE `id` = :feed'; + $query = $this->mysql->prepare($sql); + } + + $query->bindValue(':data', $prepared[0]); + $query->bindValue(':time', time()); + $query->bindValue(':feed', $this->id); + if (!$query->execute()) + { + return false; + } + } + else + { + $query = $this->mysql->prepare('INSERT INTO `' . $this->options['extras']['prefix'] . 'cache_data` (`id`, `items`, `data`, `mtime`) VALUES(:feed, :count, :data, :time)'); + $query->bindValue(':feed', $this->id); + $query->bindValue(':count', count($prepared[1])); + $query->bindValue(':data', $prepared[0]); + $query->bindValue(':time', time()); + if (!$query->execute()) + { + return false; + } + } + + $ids = array_keys($prepared[1]); + if (!empty($ids)) + { + foreach ($ids as $id) + { + $database_ids[] = $this->mysql->quote($id); + } + + $query = $this->mysql->prepare('SELECT `id` FROM `' . $this->options['extras']['prefix'] . 'items` WHERE `id` = ' . implode(' OR `id` = ', $database_ids) . ' AND `feed_id` = :feed'); + $query->bindValue(':feed', $this->id); + + if ($query->execute()) + { + $existing_ids = array(); + while ($row = $query->fetchColumn()) + { + $existing_ids[] = $row; + } + + $new_ids = array_diff($ids, $existing_ids); + + foreach ($new_ids as $new_id) + { + if (!($date = $prepared[1][$new_id]->get_date('U'))) + { + $date = time(); + } + + $query = $this->mysql->prepare('INSERT INTO `' . $this->options['extras']['prefix'] . 'items` (`feed_id`, `id`, `data`, `posted`) VALUES(:feed, :id, :data, :date)'); + $query->bindValue(':feed', $this->id); + $query->bindValue(':id', $new_id); + $query->bindValue(':data', serialize($prepared[1][$new_id]->data)); + $query->bindValue(':date', $date); + if (!$query->execute()) + { + return false; + } + } + return true; + } + } + else + { + return true; + } + } + } + else + { + $query = $this->mysql->prepare('SELECT `id` FROM `' . $this->options['extras']['prefix'] . 'cache_data` WHERE `id` = :feed'); + $query->bindValue(':feed', $this->id); + if ($query->execute()) + { + if ($query->rowCount() > 0) + { + $query = $this->mysql->prepare('UPDATE `' . $this->options['extras']['prefix'] . 'cache_data` SET `items` = 0, `data` = :data, `mtime` = :time WHERE `id` = :feed'); + $query->bindValue(':data', serialize($data)); + $query->bindValue(':time', time()); + $query->bindValue(':feed', $this->id); + if ($this->execute()) + { + return true; + } + } + else + { + $query = $this->mysql->prepare('INSERT INTO `' . $this->options['extras']['prefix'] . 'cache_data` (`id`, `items`, `data`, `mtime`) VALUES(:id, 0, :data, :time)'); + $query->bindValue(':id', $this->id); + $query->bindValue(':data', serialize($data)); + $query->bindValue(':time', time()); + if ($query->execute()) + { + return true; + } + } + } + } + return false; + } + + /** + * Retrieve the data saved to the cache + * + * @return array Data for SimplePie::$data + */ + public function load() + { + if ($this->mysql === null) + { + return false; + } + + $query = $this->mysql->prepare('SELECT `items`, `data` FROM `' . $this->options['extras']['prefix'] . 'cache_data` WHERE `id` = :id'); + $query->bindValue(':id', $this->id); + if ($query->execute() && ($row = $query->fetch())) + { + $data = unserialize($row[1]); + + if (isset($this->options['items'][0])) + { + $items = (int) $this->options['items'][0]; + } + else + { + $items = (int) $row[0]; + } + + if ($items !== 0) + { + if (isset($data['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['feed'][0])) + { + $feed =& $data['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['feed'][0]; + } + elseif (isset($data['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['feed'][0])) + { + $feed =& $data['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['feed'][0]; + } + elseif (isset($data['child'][SIMPLEPIE_NAMESPACE_RDF]['RDF'][0])) + { + $feed =& $data['child'][SIMPLEPIE_NAMESPACE_RDF]['RDF'][0]; + } + elseif (isset($data['child'][SIMPLEPIE_NAMESPACE_RSS_20]['rss'][0])) + { + $feed =& $data['child'][SIMPLEPIE_NAMESPACE_RSS_20]['rss'][0]; + } + else + { + $feed = null; + } + + if ($feed !== null) + { + $sql = 'SELECT `data` FROM `' . $this->options['extras']['prefix'] . 'items` WHERE `feed_id` = :feed ORDER BY `posted` DESC'; + if ($items > 0) + { + $sql .= ' LIMIT ' . $items; + } + + $query = $this->mysql->prepare($sql); + $query->bindValue(':feed', $this->id); + if ($query->execute()) + { + while ($row = $query->fetchColumn()) + { + $feed['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['entry'][] = unserialize($row); + } + } + else + { + return false; + } + } + } + return $data; + } + return false; + } + + /** + * Retrieve the last modified time for the cache + * + * @return int Timestamp + */ + public function mtime() + { + if ($this->mysql === null) + { + return false; + } + + $query = $this->mysql->prepare('SELECT `mtime` FROM `' . $this->options['extras']['prefix'] . 'cache_data` WHERE `id` = :id'); + $query->bindValue(':id', $this->id); + if ($query->execute() && ($time = $query->fetchColumn())) + { + return $time; + } + else + { + return false; + } + } + + /** + * Set the last modified time to the current time + * + * @return bool Success status + */ + public function touch() + { + if ($this->mysql === null) + { + return false; + } + + $query = $this->mysql->prepare('UPDATE `' . $this->options['extras']['prefix'] . 'cache_data` SET `mtime` = :time WHERE `id` = :id'); + $query->bindValue(':time', time()); + $query->bindValue(':id', $this->id); + if ($query->execute() && $query->rowCount() > 0) + { + return true; + } + else + { + return false; + } + } + + /** + * Remove the cache + * + * @return bool Success status + */ + public function unlink() + { + if ($this->mysql === null) + { + return false; + } + + $query = $this->mysql->prepare('DELETE FROM `' . $this->options['extras']['prefix'] . 'cache_data` WHERE `id` = :id'); + $query->bindValue(':id', $this->id); + $query2 = $this->mysql->prepare('DELETE FROM `' . $this->options['extras']['prefix'] . 'items` WHERE `feed_id` = :id'); + $query2->bindValue(':id', $this->id); + if ($query->execute() && $query2->execute()) + { + return true; + } + else + { + return false; + } + } +} diff --git a/vendor/simplepie/simplepie/library/SimplePie/Cache/Redis.php b/vendor/simplepie/simplepie/library/SimplePie/Cache/Redis.php new file mode 100644 index 000000000..04d72c79a --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Cache/Redis.php @@ -0,0 +1,166 @@ +<?php + +/** + * SimplePie Redis Cache Extension + * + * @package SimplePie + * @author Jan Kozak <galvani78@gmail.com> + * @link http://galvani.cz/ + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version 0.2.9 + */ + + +/** + * Caches data to redis + * + * Registered for URLs with the "redis" protocol + * + * For example, `redis://localhost:6379/?timeout=3600&prefix=sp_&dbIndex=0` will + * connect to redis on `localhost` on port 6379. All tables will be + * prefixed with `simple_primary-` and data will expire after 3600 seconds + * + * @package SimplePie + * @subpackage Caching + * @uses Redis + */ +class SimplePie_Cache_Redis implements SimplePie_Cache_Base { + /** + * Redis instance + * + * @var \Redis + */ + protected $cache; + + /** + * Options + * + * @var array + */ + protected $options; + + /** + * Cache name + * + * @var string + */ + protected $name; + + /** + * Cache Data + * + * @var type + */ + protected $data; + + /** + * Create a new cache object + * + * @param string $location Location string (from SimplePie::$cache_location) + * @param string $name Unique ID for the cache + * @param string $type Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data + */ + public function __construct($location, $name, $options = null) { + //$this->cache = \flow\simple\cache\Redis::getRedisClientInstance(); + $parsed = SimplePie_Cache::parse_URL($location); + $redis = new Redis(); + $redis->connect($parsed['host'], $parsed['port']); + $this->cache = $redis; + + if (!is_null($options) && is_array($options)) { + $this->options = $options; + } else { + $this->options = array ( + 'prefix' => 'rss:simple_primary:', + 'expire' => 0, + ); + } + + $this->name = $this->options['prefix'] . $name; + } + + /** + * @param \Redis $cache + */ + public function setRedisClient(\Redis $cache) { + $this->cache = $cache; + } + + /** + * Save data to the cache + * + * @param array|SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property + * @return bool Successfulness + */ + public function save($data) { + if ($data instanceof SimplePie) { + $data = $data->data; + } + $response = $this->cache->set($this->name, serialize($data)); + if ($this->options['expire']) { + $this->cache->expire($this->name, $this->options['expire']); + } + + return $response; + } + + /** + * Retrieve the data saved to the cache + * + * @return array Data for SimplePie::$data + */ + public function load() { + $data = $this->cache->get($this->name); + + if ($data !== false) { + return unserialize($data); + } + return false; + } + + /** + * Retrieve the last modified time for the cache + * + * @return int Timestamp + */ + public function mtime() { + + $data = $this->cache->get($this->name); + + if ($data !== false) { + return time(); + } + + return false; + } + + /** + * Set the last modified time to the current time + * + * @return bool Success status + */ + public function touch() { + + $data = $this->cache->get($this->name); + + if ($data !== false) { + $return = $this->cache->set($this->name, $data); + if ($this->options['expire']) { + return $this->cache->expire($this->name, $this->ttl); + } + return $return; + } + + return false; + } + + /** + * Remove the cache + * + * @return bool Success status + */ + public function unlink() { + return $this->cache->set($this->name, null); + } + +} diff --git a/vendor/simplepie/simplepie/library/SimplePie/Caption.php b/vendor/simplepie/simplepie/library/SimplePie/Caption.php new file mode 100644 index 000000000..abf07de1b --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Caption.php @@ -0,0 +1,209 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + + +/** + * Handles `<media:text>` captions as defined in Media RSS. + * + * Used by {@see SimplePie_Enclosure::get_caption()} and {@see SimplePie_Enclosure::get_captions()} + * + * This class can be overloaded with {@see SimplePie::set_caption_class()} + * + * @package SimplePie + * @subpackage API + */ +class SimplePie_Caption +{ + /** + * Content type + * + * @var string + * @see get_type() + */ + var $type; + + /** + * Language + * + * @var string + * @see get_language() + */ + var $lang; + + /** + * Start time + * + * @var string + * @see get_starttime() + */ + var $startTime; + + /** + * End time + * + * @var string + * @see get_endtime() + */ + var $endTime; + + /** + * Caption text + * + * @var string + * @see get_text() + */ + var $text; + + /** + * Constructor, used to input the data + * + * For documentation on all the parameters, see the corresponding + * properties and their accessors + */ + public function __construct($type = null, $lang = null, $startTime = null, $endTime = null, $text = null) + { + $this->type = $type; + $this->lang = $lang; + $this->startTime = $startTime; + $this->endTime = $endTime; + $this->text = $text; + } + + /** + * String-ified version + * + * @return string + */ + public function __toString() + { + // There is no $this->data here + return md5(serialize($this)); + } + + /** + * Get the end time + * + * @return string|null Time in the format 'hh:mm:ss.SSS' + */ + public function get_endtime() + { + if ($this->endTime !== null) + { + return $this->endTime; + } + else + { + return null; + } + } + + /** + * Get the language + * + * @link http://tools.ietf.org/html/rfc3066 + * @return string|null Language code as per RFC 3066 + */ + public function get_language() + { + if ($this->lang !== null) + { + return $this->lang; + } + else + { + return null; + } + } + + /** + * Get the start time + * + * @return string|null Time in the format 'hh:mm:ss.SSS' + */ + public function get_starttime() + { + if ($this->startTime !== null) + { + return $this->startTime; + } + else + { + return null; + } + } + + /** + * Get the text of the caption + * + * @return string|null + */ + public function get_text() + { + if ($this->text !== null) + { + return $this->text; + } + else + { + return null; + } + } + + /** + * Get the content type (not MIME type) + * + * @return string|null Either 'text' or 'html' + */ + public function get_type() + { + if ($this->type !== null) + { + return $this->type; + } + else + { + return null; + } + } +} + diff --git a/vendor/simplepie/simplepie/library/SimplePie/Category.php b/vendor/simplepie/simplepie/library/SimplePie/Category.php new file mode 100644 index 000000000..df0f13f9a --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Category.php @@ -0,0 +1,163 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Manages all category-related data + * + * Used by {@see SimplePie_Item::get_category()} and {@see SimplePie_Item::get_categories()} + * + * This class can be overloaded with {@see SimplePie::set_category_class()} + * + * @package SimplePie + * @subpackage API + */ +class SimplePie_Category +{ + /** + * Category identifier + * + * @var string|null + * @see get_term + */ + var $term; + + /** + * Categorization scheme identifier + * + * @var string|null + * @see get_scheme() + */ + var $scheme; + + /** + * Human readable label + * + * @var string|null + * @see get_label() + */ + var $label; + + /** + * Category type + * + * category for <category> + * subject for <dc:subject> + * + * @var string|null + * @see get_type() + */ + var $type; + + /** + * Constructor, used to input the data + * + * @param string|null $term + * @param string|null $scheme + * @param string|null $label + * @param string|null $type + */ + public function __construct($term = null, $scheme = null, $label = null, $type = null) + { + $this->term = $term; + $this->scheme = $scheme; + $this->label = $label; + $this->type = $type; + } + + /** + * String-ified version + * + * @return string + */ + public function __toString() + { + // There is no $this->data here + return md5(serialize($this)); + } + + /** + * Get the category identifier + * + * @return string|null + */ + public function get_term() + { + return $this->term; + } + + /** + * Get the categorization scheme identifier + * + * @return string|null + */ + public function get_scheme() + { + return $this->scheme; + } + + /** + * Get the human readable label + * + * @param bool $strict + * @return string|null + */ + public function get_label($strict = false) + { + if ($this->label === null && $strict !== true) + { + return $this->get_term(); + } + return $this->label; + } + + /** + * Get the category type + * + * @return string|null + */ + public function get_type() + { + return $this->type; + } +} + diff --git a/vendor/simplepie/simplepie/library/SimplePie/Content/Type/Sniffer.php b/vendor/simplepie/simplepie/library/SimplePie/Content/Type/Sniffer.php new file mode 100644 index 000000000..ff35de614 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Content/Type/Sniffer.php @@ -0,0 +1,331 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + + +/** + * Content-type sniffing + * + * Based on the rules in http://tools.ietf.org/html/draft-abarth-mime-sniff-06 + * + * This is used since we can't always trust Content-Type headers, and is based + * upon the HTML5 parsing rules. + * + * + * This class can be overloaded with {@see SimplePie::set_content_type_sniffer_class()} + * + * @package SimplePie + * @subpackage HTTP + */ +class SimplePie_Content_Type_Sniffer +{ + /** + * File object + * + * @var SimplePie_File + */ + var $file; + + /** + * Create an instance of the class with the input file + * + * @param SimplePie_Content_Type_Sniffer $file Input file + */ + public function __construct($file) + { + $this->file = $file; + } + + /** + * Get the Content-Type of the specified file + * + * @return string Actual Content-Type + */ + public function get_type() + { + if (isset($this->file->headers['content-type'])) + { + if (!isset($this->file->headers['content-encoding']) + && ($this->file->headers['content-type'] === 'text/plain' + || $this->file->headers['content-type'] === 'text/plain; charset=ISO-8859-1' + || $this->file->headers['content-type'] === 'text/plain; charset=iso-8859-1' + || $this->file->headers['content-type'] === 'text/plain; charset=UTF-8')) + { + return $this->text_or_binary(); + } + + if (($pos = strpos($this->file->headers['content-type'], ';')) !== false) + { + $official = substr($this->file->headers['content-type'], 0, $pos); + } + else + { + $official = $this->file->headers['content-type']; + } + $official = trim(strtolower($official)); + + if ($official === 'unknown/unknown' + || $official === 'application/unknown') + { + return $this->unknown(); + } + elseif (substr($official, -4) === '+xml' + || $official === 'text/xml' + || $official === 'application/xml') + { + return $official; + } + elseif (substr($official, 0, 6) === 'image/') + { + if ($return = $this->image()) + { + return $return; + } + else + { + return $official; + } + } + elseif ($official === 'text/html') + { + return $this->feed_or_html(); + } + else + { + return $official; + } + } + else + { + return $this->unknown(); + } + } + + /** + * Sniff text or binary + * + * @return string Actual Content-Type + */ + public function text_or_binary() + { + if (substr($this->file->body, 0, 2) === "\xFE\xFF" + || substr($this->file->body, 0, 2) === "\xFF\xFE" + || substr($this->file->body, 0, 4) === "\x00\x00\xFE\xFF" + || substr($this->file->body, 0, 3) === "\xEF\xBB\xBF") + { + return 'text/plain'; + } + elseif (preg_match('/[\x00-\x08\x0E-\x1A\x1C-\x1F]/', $this->file->body)) + { + return 'application/octect-stream'; + } + else + { + return 'text/plain'; + } + } + + /** + * Sniff unknown + * + * @return string Actual Content-Type + */ + public function unknown() + { + $ws = strspn($this->file->body, "\x09\x0A\x0B\x0C\x0D\x20"); + if (strtolower(substr($this->file->body, $ws, 14)) === '<!doctype html' + || strtolower(substr($this->file->body, $ws, 5)) === '<html' + || strtolower(substr($this->file->body, $ws, 7)) === '<script') + { + return 'text/html'; + } + elseif (substr($this->file->body, 0, 5) === '%PDF-') + { + return 'application/pdf'; + } + elseif (substr($this->file->body, 0, 11) === '%!PS-Adobe-') + { + return 'application/postscript'; + } + elseif (substr($this->file->body, 0, 6) === 'GIF87a' + || substr($this->file->body, 0, 6) === 'GIF89a') + { + return 'image/gif'; + } + elseif (substr($this->file->body, 0, 8) === "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A") + { + return 'image/png'; + } + elseif (substr($this->file->body, 0, 3) === "\xFF\xD8\xFF") + { + return 'image/jpeg'; + } + elseif (substr($this->file->body, 0, 2) === "\x42\x4D") + { + return 'image/bmp'; + } + elseif (substr($this->file->body, 0, 4) === "\x00\x00\x01\x00") + { + return 'image/vnd.microsoft.icon'; + } + else + { + return $this->text_or_binary(); + } + } + + /** + * Sniff images + * + * @return string Actual Content-Type + */ + public function image() + { + if (substr($this->file->body, 0, 6) === 'GIF87a' + || substr($this->file->body, 0, 6) === 'GIF89a') + { + return 'image/gif'; + } + elseif (substr($this->file->body, 0, 8) === "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A") + { + return 'image/png'; + } + elseif (substr($this->file->body, 0, 3) === "\xFF\xD8\xFF") + { + return 'image/jpeg'; + } + elseif (substr($this->file->body, 0, 2) === "\x42\x4D") + { + return 'image/bmp'; + } + elseif (substr($this->file->body, 0, 4) === "\x00\x00\x01\x00") + { + return 'image/vnd.microsoft.icon'; + } + else + { + return false; + } + } + + /** + * Sniff HTML + * + * @return string Actual Content-Type + */ + public function feed_or_html() + { + $len = strlen($this->file->body); + $pos = strspn($this->file->body, "\x09\x0A\x0D\x20\xEF\xBB\xBF"); + + while ($pos < $len) + { + switch ($this->file->body[$pos]) + { + case "\x09": + case "\x0A": + case "\x0D": + case "\x20": + $pos += strspn($this->file->body, "\x09\x0A\x0D\x20", $pos); + continue 2; + + case '<': + $pos++; + break; + + default: + return 'text/html'; + } + + if (substr($this->file->body, $pos, 3) === '!--') + { + $pos += 3; + if ($pos < $len && ($pos = strpos($this->file->body, '-->', $pos)) !== false) + { + $pos += 3; + } + else + { + return 'text/html'; + } + } + elseif (substr($this->file->body, $pos, 1) === '!') + { + if ($pos < $len && ($pos = strpos($this->file->body, '>', $pos)) !== false) + { + $pos++; + } + else + { + return 'text/html'; + } + } + elseif (substr($this->file->body, $pos, 1) === '?') + { + if ($pos < $len && ($pos = strpos($this->file->body, '?>', $pos)) !== false) + { + $pos += 2; + } + else + { + return 'text/html'; + } + } + elseif (substr($this->file->body, $pos, 3) === 'rss' + || substr($this->file->body, $pos, 7) === 'rdf:RDF') + { + return 'application/rss+xml'; + } + elseif (substr($this->file->body, $pos, 4) === 'feed') + { + return 'application/atom+xml'; + } + else + { + return 'text/html'; + } + } + + return 'text/html'; + } +} + diff --git a/vendor/simplepie/simplepie/library/SimplePie/Copyright.php b/vendor/simplepie/simplepie/library/SimplePie/Copyright.php new file mode 100644 index 000000000..3f3d07d3b --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Copyright.php @@ -0,0 +1,129 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Manages `<media:copyright>` copyright tags as defined in Media RSS + * + * Used by {@see SimplePie_Enclosure::get_copyright()} + * + * This class can be overloaded with {@see SimplePie::set_copyright_class()} + * + * @package SimplePie + * @subpackage API + */ +class SimplePie_Copyright +{ + /** + * Copyright URL + * + * @var string + * @see get_url() + */ + var $url; + + /** + * Attribution + * + * @var string + * @see get_attribution() + */ + var $label; + + /** + * Constructor, used to input the data + * + * For documentation on all the parameters, see the corresponding + * properties and their accessors + */ + public function __construct($url = null, $label = null) + { + $this->url = $url; + $this->label = $label; + } + + /** + * String-ified version + * + * @return string + */ + public function __toString() + { + // There is no $this->data here + return md5(serialize($this)); + } + + /** + * Get the copyright URL + * + * @return string|null URL to copyright information + */ + public function get_url() + { + if ($this->url !== null) + { + return $this->url; + } + else + { + return null; + } + } + + /** + * Get the attribution text + * + * @return string|null + */ + public function get_attribution() + { + if ($this->label !== null) + { + return $this->label; + } + else + { + return null; + } + } +} + diff --git a/vendor/simplepie/simplepie/library/SimplePie/Core.php b/vendor/simplepie/simplepie/library/SimplePie/Core.php new file mode 100644 index 000000000..c856ba361 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Core.php @@ -0,0 +1,56 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * SimplePie class. + * + * Class for backward compatibility. + * + * @deprecated Use {@see SimplePie} directly + * @package SimplePie + * @subpackage API + */ +class SimplePie_Core extends SimplePie +{ + +}
\ No newline at end of file diff --git a/vendor/simplepie/simplepie/library/SimplePie/Credit.php b/vendor/simplepie/simplepie/library/SimplePie/Credit.php new file mode 100644 index 000000000..9bad9ef34 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Credit.php @@ -0,0 +1,155 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Handles `<media:credit>` as defined in Media RSS + * + * Used by {@see SimplePie_Enclosure::get_credit()} and {@see SimplePie_Enclosure::get_credits()} + * + * This class can be overloaded with {@see SimplePie::set_credit_class()} + * + * @package SimplePie + * @subpackage API + */ +class SimplePie_Credit +{ + /** + * Credited role + * + * @var string + * @see get_role() + */ + var $role; + + /** + * Organizational scheme + * + * @var string + * @see get_scheme() + */ + var $scheme; + + /** + * Credited name + * + * @var string + * @see get_name() + */ + var $name; + + /** + * Constructor, used to input the data + * + * For documentation on all the parameters, see the corresponding + * properties and their accessors + */ + public function __construct($role = null, $scheme = null, $name = null) + { + $this->role = $role; + $this->scheme = $scheme; + $this->name = $name; + } + + /** + * String-ified version + * + * @return string + */ + public function __toString() + { + // There is no $this->data here + return md5(serialize($this)); + } + + /** + * Get the role of the person receiving credit + * + * @return string|null + */ + public function get_role() + { + if ($this->role !== null) + { + return $this->role; + } + else + { + return null; + } + } + + /** + * Get the organizational scheme + * + * @return string|null + */ + public function get_scheme() + { + if ($this->scheme !== null) + { + return $this->scheme; + } + else + { + return null; + } + } + + /** + * Get the credited person/entity's name + * + * @return string|null + */ + public function get_name() + { + if ($this->name !== null) + { + return $this->name; + } + else + { + return null; + } + } +} + diff --git a/vendor/simplepie/simplepie/library/SimplePie/Decode/HTML/Entities.php b/vendor/simplepie/simplepie/library/SimplePie/Decode/HTML/Entities.php new file mode 100644 index 000000000..de3f2cb53 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Decode/HTML/Entities.php @@ -0,0 +1,615 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + + +/** + * Decode HTML Entities + * + * This implements HTML5 as of revision 967 (2007-06-28) + * + * @deprecated Use DOMDocument instead! + * @package SimplePie + */ +class SimplePie_Decode_HTML_Entities +{ + /** + * Data to be parsed + * + * @access private + * @var string + */ + var $data = ''; + + /** + * Currently consumed bytes + * + * @access private + * @var string + */ + var $consumed = ''; + + /** + * Position of the current byte being parsed + * + * @access private + * @var int + */ + var $position = 0; + + /** + * Create an instance of the class with the input data + * + * @access public + * @param string $data Input data + */ + public function __construct($data) + { + $this->data = $data; + } + + /** + * Parse the input data + * + * @access public + * @return string Output data + */ + public function parse() + { + while (($this->position = strpos($this->data, '&', $this->position)) !== false) + { + $this->consume(); + $this->entity(); + $this->consumed = ''; + } + return $this->data; + } + + /** + * Consume the next byte + * + * @access private + * @return mixed The next byte, or false, if there is no more data + */ + public function consume() + { + if (isset($this->data[$this->position])) + { + $this->consumed .= $this->data[$this->position]; + return $this->data[$this->position++]; + } + else + { + return false; + } + } + + /** + * Consume a range of characters + * + * @access private + * @param string $chars Characters to consume + * @return mixed A series of characters that match the range, or false + */ + public function consume_range($chars) + { + if ($len = strspn($this->data, $chars, $this->position)) + { + $data = substr($this->data, $this->position, $len); + $this->consumed .= $data; + $this->position += $len; + return $data; + } + else + { + return false; + } + } + + /** + * Unconsume one byte + * + * @access private + */ + public function unconsume() + { + $this->consumed = substr($this->consumed, 0, -1); + $this->position--; + } + + /** + * Decode an entity + * + * @access private + */ + public function entity() + { + switch ($this->consume()) + { + case "\x09": + case "\x0A": + case "\x0B": + case "\x0C": + case "\x20": + case "\x3C": + case "\x26": + case false: + break; + + case "\x23": + switch ($this->consume()) + { + case "\x78": + case "\x58": + $range = '0123456789ABCDEFabcdef'; + $hex = true; + break; + + default: + $range = '0123456789'; + $hex = false; + $this->unconsume(); + break; + } + + if ($codepoint = $this->consume_range($range)) + { + static $windows_1252_specials = array(0x0D => "\x0A", 0x80 => "\xE2\x82\xAC", 0x81 => "\xEF\xBF\xBD", 0x82 => "\xE2\x80\x9A", 0x83 => "\xC6\x92", 0x84 => "\xE2\x80\x9E", 0x85 => "\xE2\x80\xA6", 0x86 => "\xE2\x80\xA0", 0x87 => "\xE2\x80\xA1", 0x88 => "\xCB\x86", 0x89 => "\xE2\x80\xB0", 0x8A => "\xC5\xA0", 0x8B => "\xE2\x80\xB9", 0x8C => "\xC5\x92", 0x8D => "\xEF\xBF\xBD", 0x8E => "\xC5\xBD", 0x8F => "\xEF\xBF\xBD", 0x90 => "\xEF\xBF\xBD", 0x91 => "\xE2\x80\x98", 0x92 => "\xE2\x80\x99", 0x93 => "\xE2\x80\x9C", 0x94 => "\xE2\x80\x9D", 0x95 => "\xE2\x80\xA2", 0x96 => "\xE2\x80\x93", 0x97 => "\xE2\x80\x94", 0x98 => "\xCB\x9C", 0x99 => "\xE2\x84\xA2", 0x9A => "\xC5\xA1", 0x9B => "\xE2\x80\xBA", 0x9C => "\xC5\x93", 0x9D => "\xEF\xBF\xBD", 0x9E => "\xC5\xBE", 0x9F => "\xC5\xB8"); + + if ($hex) + { + $codepoint = hexdec($codepoint); + } + else + { + $codepoint = intval($codepoint); + } + + if (isset($windows_1252_specials[$codepoint])) + { + $replacement = $windows_1252_specials[$codepoint]; + } + else + { + $replacement = SimplePie_Misc::codepoint_to_utf8($codepoint); + } + + if (!in_array($this->consume(), array(';', false), true)) + { + $this->unconsume(); + } + + $consumed_length = strlen($this->consumed); + $this->data = substr_replace($this->data, $replacement, $this->position - $consumed_length, $consumed_length); + $this->position += strlen($replacement) - $consumed_length; + } + break; + + default: + static $entities = array( + 'Aacute' => "\xC3\x81", + 'aacute' => "\xC3\xA1", + 'Aacute;' => "\xC3\x81", + 'aacute;' => "\xC3\xA1", + 'Acirc' => "\xC3\x82", + 'acirc' => "\xC3\xA2", + 'Acirc;' => "\xC3\x82", + 'acirc;' => "\xC3\xA2", + 'acute' => "\xC2\xB4", + 'acute;' => "\xC2\xB4", + 'AElig' => "\xC3\x86", + 'aelig' => "\xC3\xA6", + 'AElig;' => "\xC3\x86", + 'aelig;' => "\xC3\xA6", + 'Agrave' => "\xC3\x80", + 'agrave' => "\xC3\xA0", + 'Agrave;' => "\xC3\x80", + 'agrave;' => "\xC3\xA0", + 'alefsym;' => "\xE2\x84\xB5", + 'Alpha;' => "\xCE\x91", + 'alpha;' => "\xCE\xB1", + 'AMP' => "\x26", + 'amp' => "\x26", + 'AMP;' => "\x26", + 'amp;' => "\x26", + 'and;' => "\xE2\x88\xA7", + 'ang;' => "\xE2\x88\xA0", + 'apos;' => "\x27", + 'Aring' => "\xC3\x85", + 'aring' => "\xC3\xA5", + 'Aring;' => "\xC3\x85", + 'aring;' => "\xC3\xA5", + 'asymp;' => "\xE2\x89\x88", + 'Atilde' => "\xC3\x83", + 'atilde' => "\xC3\xA3", + 'Atilde;' => "\xC3\x83", + 'atilde;' => "\xC3\xA3", + 'Auml' => "\xC3\x84", + 'auml' => "\xC3\xA4", + 'Auml;' => "\xC3\x84", + 'auml;' => "\xC3\xA4", + 'bdquo;' => "\xE2\x80\x9E", + 'Beta;' => "\xCE\x92", + 'beta;' => "\xCE\xB2", + 'brvbar' => "\xC2\xA6", + 'brvbar;' => "\xC2\xA6", + 'bull;' => "\xE2\x80\xA2", + 'cap;' => "\xE2\x88\xA9", + 'Ccedil' => "\xC3\x87", + 'ccedil' => "\xC3\xA7", + 'Ccedil;' => "\xC3\x87", + 'ccedil;' => "\xC3\xA7", + 'cedil' => "\xC2\xB8", + 'cedil;' => "\xC2\xB8", + 'cent' => "\xC2\xA2", + 'cent;' => "\xC2\xA2", + 'Chi;' => "\xCE\xA7", + 'chi;' => "\xCF\x87", + 'circ;' => "\xCB\x86", + 'clubs;' => "\xE2\x99\xA3", + 'cong;' => "\xE2\x89\x85", + 'COPY' => "\xC2\xA9", + 'copy' => "\xC2\xA9", + 'COPY;' => "\xC2\xA9", + 'copy;' => "\xC2\xA9", + 'crarr;' => "\xE2\x86\xB5", + 'cup;' => "\xE2\x88\xAA", + 'curren' => "\xC2\xA4", + 'curren;' => "\xC2\xA4", + 'Dagger;' => "\xE2\x80\xA1", + 'dagger;' => "\xE2\x80\xA0", + 'dArr;' => "\xE2\x87\x93", + 'darr;' => "\xE2\x86\x93", + 'deg' => "\xC2\xB0", + 'deg;' => "\xC2\xB0", + 'Delta;' => "\xCE\x94", + 'delta;' => "\xCE\xB4", + 'diams;' => "\xE2\x99\xA6", + 'divide' => "\xC3\xB7", + 'divide;' => "\xC3\xB7", + 'Eacute' => "\xC3\x89", + 'eacute' => "\xC3\xA9", + 'Eacute;' => "\xC3\x89", + 'eacute;' => "\xC3\xA9", + 'Ecirc' => "\xC3\x8A", + 'ecirc' => "\xC3\xAA", + 'Ecirc;' => "\xC3\x8A", + 'ecirc;' => "\xC3\xAA", + 'Egrave' => "\xC3\x88", + 'egrave' => "\xC3\xA8", + 'Egrave;' => "\xC3\x88", + 'egrave;' => "\xC3\xA8", + 'empty;' => "\xE2\x88\x85", + 'emsp;' => "\xE2\x80\x83", + 'ensp;' => "\xE2\x80\x82", + 'Epsilon;' => "\xCE\x95", + 'epsilon;' => "\xCE\xB5", + 'equiv;' => "\xE2\x89\xA1", + 'Eta;' => "\xCE\x97", + 'eta;' => "\xCE\xB7", + 'ETH' => "\xC3\x90", + 'eth' => "\xC3\xB0", + 'ETH;' => "\xC3\x90", + 'eth;' => "\xC3\xB0", + 'Euml' => "\xC3\x8B", + 'euml' => "\xC3\xAB", + 'Euml;' => "\xC3\x8B", + 'euml;' => "\xC3\xAB", + 'euro;' => "\xE2\x82\xAC", + 'exist;' => "\xE2\x88\x83", + 'fnof;' => "\xC6\x92", + 'forall;' => "\xE2\x88\x80", + 'frac12' => "\xC2\xBD", + 'frac12;' => "\xC2\xBD", + 'frac14' => "\xC2\xBC", + 'frac14;' => "\xC2\xBC", + 'frac34' => "\xC2\xBE", + 'frac34;' => "\xC2\xBE", + 'frasl;' => "\xE2\x81\x84", + 'Gamma;' => "\xCE\x93", + 'gamma;' => "\xCE\xB3", + 'ge;' => "\xE2\x89\xA5", + 'GT' => "\x3E", + 'gt' => "\x3E", + 'GT;' => "\x3E", + 'gt;' => "\x3E", + 'hArr;' => "\xE2\x87\x94", + 'harr;' => "\xE2\x86\x94", + 'hearts;' => "\xE2\x99\xA5", + 'hellip;' => "\xE2\x80\xA6", + 'Iacute' => "\xC3\x8D", + 'iacute' => "\xC3\xAD", + 'Iacute;' => "\xC3\x8D", + 'iacute;' => "\xC3\xAD", + 'Icirc' => "\xC3\x8E", + 'icirc' => "\xC3\xAE", + 'Icirc;' => "\xC3\x8E", + 'icirc;' => "\xC3\xAE", + 'iexcl' => "\xC2\xA1", + 'iexcl;' => "\xC2\xA1", + 'Igrave' => "\xC3\x8C", + 'igrave' => "\xC3\xAC", + 'Igrave;' => "\xC3\x8C", + 'igrave;' => "\xC3\xAC", + 'image;' => "\xE2\x84\x91", + 'infin;' => "\xE2\x88\x9E", + 'int;' => "\xE2\x88\xAB", + 'Iota;' => "\xCE\x99", + 'iota;' => "\xCE\xB9", + 'iquest' => "\xC2\xBF", + 'iquest;' => "\xC2\xBF", + 'isin;' => "\xE2\x88\x88", + 'Iuml' => "\xC3\x8F", + 'iuml' => "\xC3\xAF", + 'Iuml;' => "\xC3\x8F", + 'iuml;' => "\xC3\xAF", + 'Kappa;' => "\xCE\x9A", + 'kappa;' => "\xCE\xBA", + 'Lambda;' => "\xCE\x9B", + 'lambda;' => "\xCE\xBB", + 'lang;' => "\xE3\x80\x88", + 'laquo' => "\xC2\xAB", + 'laquo;' => "\xC2\xAB", + 'lArr;' => "\xE2\x87\x90", + 'larr;' => "\xE2\x86\x90", + 'lceil;' => "\xE2\x8C\x88", + 'ldquo;' => "\xE2\x80\x9C", + 'le;' => "\xE2\x89\xA4", + 'lfloor;' => "\xE2\x8C\x8A", + 'lowast;' => "\xE2\x88\x97", + 'loz;' => "\xE2\x97\x8A", + 'lrm;' => "\xE2\x80\x8E", + 'lsaquo;' => "\xE2\x80\xB9", + 'lsquo;' => "\xE2\x80\x98", + 'LT' => "\x3C", + 'lt' => "\x3C", + 'LT;' => "\x3C", + 'lt;' => "\x3C", + 'macr' => "\xC2\xAF", + 'macr;' => "\xC2\xAF", + 'mdash;' => "\xE2\x80\x94", + 'micro' => "\xC2\xB5", + 'micro;' => "\xC2\xB5", + 'middot' => "\xC2\xB7", + 'middot;' => "\xC2\xB7", + 'minus;' => "\xE2\x88\x92", + 'Mu;' => "\xCE\x9C", + 'mu;' => "\xCE\xBC", + 'nabla;' => "\xE2\x88\x87", + 'nbsp' => "\xC2\xA0", + 'nbsp;' => "\xC2\xA0", + 'ndash;' => "\xE2\x80\x93", + 'ne;' => "\xE2\x89\xA0", + 'ni;' => "\xE2\x88\x8B", + 'not' => "\xC2\xAC", + 'not;' => "\xC2\xAC", + 'notin;' => "\xE2\x88\x89", + 'nsub;' => "\xE2\x8A\x84", + 'Ntilde' => "\xC3\x91", + 'ntilde' => "\xC3\xB1", + 'Ntilde;' => "\xC3\x91", + 'ntilde;' => "\xC3\xB1", + 'Nu;' => "\xCE\x9D", + 'nu;' => "\xCE\xBD", + 'Oacute' => "\xC3\x93", + 'oacute' => "\xC3\xB3", + 'Oacute;' => "\xC3\x93", + 'oacute;' => "\xC3\xB3", + 'Ocirc' => "\xC3\x94", + 'ocirc' => "\xC3\xB4", + 'Ocirc;' => "\xC3\x94", + 'ocirc;' => "\xC3\xB4", + 'OElig;' => "\xC5\x92", + 'oelig;' => "\xC5\x93", + 'Ograve' => "\xC3\x92", + 'ograve' => "\xC3\xB2", + 'Ograve;' => "\xC3\x92", + 'ograve;' => "\xC3\xB2", + 'oline;' => "\xE2\x80\xBE", + 'Omega;' => "\xCE\xA9", + 'omega;' => "\xCF\x89", + 'Omicron;' => "\xCE\x9F", + 'omicron;' => "\xCE\xBF", + 'oplus;' => "\xE2\x8A\x95", + 'or;' => "\xE2\x88\xA8", + 'ordf' => "\xC2\xAA", + 'ordf;' => "\xC2\xAA", + 'ordm' => "\xC2\xBA", + 'ordm;' => "\xC2\xBA", + 'Oslash' => "\xC3\x98", + 'oslash' => "\xC3\xB8", + 'Oslash;' => "\xC3\x98", + 'oslash;' => "\xC3\xB8", + 'Otilde' => "\xC3\x95", + 'otilde' => "\xC3\xB5", + 'Otilde;' => "\xC3\x95", + 'otilde;' => "\xC3\xB5", + 'otimes;' => "\xE2\x8A\x97", + 'Ouml' => "\xC3\x96", + 'ouml' => "\xC3\xB6", + 'Ouml;' => "\xC3\x96", + 'ouml;' => "\xC3\xB6", + 'para' => "\xC2\xB6", + 'para;' => "\xC2\xB6", + 'part;' => "\xE2\x88\x82", + 'permil;' => "\xE2\x80\xB0", + 'perp;' => "\xE2\x8A\xA5", + 'Phi;' => "\xCE\xA6", + 'phi;' => "\xCF\x86", + 'Pi;' => "\xCE\xA0", + 'pi;' => "\xCF\x80", + 'piv;' => "\xCF\x96", + 'plusmn' => "\xC2\xB1", + 'plusmn;' => "\xC2\xB1", + 'pound' => "\xC2\xA3", + 'pound;' => "\xC2\xA3", + 'Prime;' => "\xE2\x80\xB3", + 'prime;' => "\xE2\x80\xB2", + 'prod;' => "\xE2\x88\x8F", + 'prop;' => "\xE2\x88\x9D", + 'Psi;' => "\xCE\xA8", + 'psi;' => "\xCF\x88", + 'QUOT' => "\x22", + 'quot' => "\x22", + 'QUOT;' => "\x22", + 'quot;' => "\x22", + 'radic;' => "\xE2\x88\x9A", + 'rang;' => "\xE3\x80\x89", + 'raquo' => "\xC2\xBB", + 'raquo;' => "\xC2\xBB", + 'rArr;' => "\xE2\x87\x92", + 'rarr;' => "\xE2\x86\x92", + 'rceil;' => "\xE2\x8C\x89", + 'rdquo;' => "\xE2\x80\x9D", + 'real;' => "\xE2\x84\x9C", + 'REG' => "\xC2\xAE", + 'reg' => "\xC2\xAE", + 'REG;' => "\xC2\xAE", + 'reg;' => "\xC2\xAE", + 'rfloor;' => "\xE2\x8C\x8B", + 'Rho;' => "\xCE\xA1", + 'rho;' => "\xCF\x81", + 'rlm;' => "\xE2\x80\x8F", + 'rsaquo;' => "\xE2\x80\xBA", + 'rsquo;' => "\xE2\x80\x99", + 'sbquo;' => "\xE2\x80\x9A", + 'Scaron;' => "\xC5\xA0", + 'scaron;' => "\xC5\xA1", + 'sdot;' => "\xE2\x8B\x85", + 'sect' => "\xC2\xA7", + 'sect;' => "\xC2\xA7", + 'shy' => "\xC2\xAD", + 'shy;' => "\xC2\xAD", + 'Sigma;' => "\xCE\xA3", + 'sigma;' => "\xCF\x83", + 'sigmaf;' => "\xCF\x82", + 'sim;' => "\xE2\x88\xBC", + 'spades;' => "\xE2\x99\xA0", + 'sub;' => "\xE2\x8A\x82", + 'sube;' => "\xE2\x8A\x86", + 'sum;' => "\xE2\x88\x91", + 'sup;' => "\xE2\x8A\x83", + 'sup1' => "\xC2\xB9", + 'sup1;' => "\xC2\xB9", + 'sup2' => "\xC2\xB2", + 'sup2;' => "\xC2\xB2", + 'sup3' => "\xC2\xB3", + 'sup3;' => "\xC2\xB3", + 'supe;' => "\xE2\x8A\x87", + 'szlig' => "\xC3\x9F", + 'szlig;' => "\xC3\x9F", + 'Tau;' => "\xCE\xA4", + 'tau;' => "\xCF\x84", + 'there4;' => "\xE2\x88\xB4", + 'Theta;' => "\xCE\x98", + 'theta;' => "\xCE\xB8", + 'thetasym;' => "\xCF\x91", + 'thinsp;' => "\xE2\x80\x89", + 'THORN' => "\xC3\x9E", + 'thorn' => "\xC3\xBE", + 'THORN;' => "\xC3\x9E", + 'thorn;' => "\xC3\xBE", + 'tilde;' => "\xCB\x9C", + 'times' => "\xC3\x97", + 'times;' => "\xC3\x97", + 'TRADE;' => "\xE2\x84\xA2", + 'trade;' => "\xE2\x84\xA2", + 'Uacute' => "\xC3\x9A", + 'uacute' => "\xC3\xBA", + 'Uacute;' => "\xC3\x9A", + 'uacute;' => "\xC3\xBA", + 'uArr;' => "\xE2\x87\x91", + 'uarr;' => "\xE2\x86\x91", + 'Ucirc' => "\xC3\x9B", + 'ucirc' => "\xC3\xBB", + 'Ucirc;' => "\xC3\x9B", + 'ucirc;' => "\xC3\xBB", + 'Ugrave' => "\xC3\x99", + 'ugrave' => "\xC3\xB9", + 'Ugrave;' => "\xC3\x99", + 'ugrave;' => "\xC3\xB9", + 'uml' => "\xC2\xA8", + 'uml;' => "\xC2\xA8", + 'upsih;' => "\xCF\x92", + 'Upsilon;' => "\xCE\xA5", + 'upsilon;' => "\xCF\x85", + 'Uuml' => "\xC3\x9C", + 'uuml' => "\xC3\xBC", + 'Uuml;' => "\xC3\x9C", + 'uuml;' => "\xC3\xBC", + 'weierp;' => "\xE2\x84\x98", + 'Xi;' => "\xCE\x9E", + 'xi;' => "\xCE\xBE", + 'Yacute' => "\xC3\x9D", + 'yacute' => "\xC3\xBD", + 'Yacute;' => "\xC3\x9D", + 'yacute;' => "\xC3\xBD", + 'yen' => "\xC2\xA5", + 'yen;' => "\xC2\xA5", + 'yuml' => "\xC3\xBF", + 'Yuml;' => "\xC5\xB8", + 'yuml;' => "\xC3\xBF", + 'Zeta;' => "\xCE\x96", + 'zeta;' => "\xCE\xB6", + 'zwj;' => "\xE2\x80\x8D", + 'zwnj;' => "\xE2\x80\x8C" + ); + + for ($i = 0, $match = null; $i < 9 && $this->consume() !== false; $i++) + { + $consumed = substr($this->consumed, 1); + if (isset($entities[$consumed])) + { + $match = $consumed; + } + } + + if ($match !== null) + { + $this->data = substr_replace($this->data, $entities[$match], $this->position - strlen($consumed) - 1, strlen($match) + 1); + $this->position += strlen($entities[$match]) - strlen($consumed) - 1; + } + break; + } + } +} + diff --git a/vendor/simplepie/simplepie/library/SimplePie/Enclosure.php b/vendor/simplepie/simplepie/library/SimplePie/Enclosure.php new file mode 100644 index 000000000..15060e193 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Enclosure.php @@ -0,0 +1,1379 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Handles everything related to enclosures (including Media RSS and iTunes RSS) + * + * Used by {@see SimplePie_Item::get_enclosure()} and {@see SimplePie_Item::get_enclosures()} + * + * This class can be overloaded with {@see SimplePie::set_enclosure_class()} + * + * @package SimplePie + * @subpackage API + */ +class SimplePie_Enclosure +{ + /** + * @var string + * @see get_bitrate() + */ + var $bitrate; + + /** + * @var array + * @see get_captions() + */ + var $captions; + + /** + * @var array + * @see get_categories() + */ + var $categories; + + /** + * @var int + * @see get_channels() + */ + var $channels; + + /** + * @var SimplePie_Copyright + * @see get_copyright() + */ + var $copyright; + + /** + * @var array + * @see get_credits() + */ + var $credits; + + /** + * @var string + * @see get_description() + */ + var $description; + + /** + * @var int + * @see get_duration() + */ + var $duration; + + /** + * @var string + * @see get_expression() + */ + var $expression; + + /** + * @var string + * @see get_framerate() + */ + var $framerate; + + /** + * @var string + * @see get_handler() + */ + var $handler; + + /** + * @var array + * @see get_hashes() + */ + var $hashes; + + /** + * @var string + * @see get_height() + */ + var $height; + + /** + * @deprecated + * @var null + */ + var $javascript; + + /** + * @var array + * @see get_keywords() + */ + var $keywords; + + /** + * @var string + * @see get_language() + */ + var $lang; + + /** + * @var string + * @see get_length() + */ + var $length; + + /** + * @var string + * @see get_link() + */ + var $link; + + /** + * @var string + * @see get_medium() + */ + var $medium; + + /** + * @var string + * @see get_player() + */ + var $player; + + /** + * @var array + * @see get_ratings() + */ + var $ratings; + + /** + * @var array + * @see get_restrictions() + */ + var $restrictions; + + /** + * @var string + * @see get_sampling_rate() + */ + var $samplingrate; + + /** + * @var array + * @see get_thumbnails() + */ + var $thumbnails; + + /** + * @var string + * @see get_title() + */ + var $title; + + /** + * @var string + * @see get_type() + */ + var $type; + + /** + * @var string + * @see get_width() + */ + var $width; + + /** + * Constructor, used to input the data + * + * For documentation on all the parameters, see the corresponding + * properties and their accessors + * + * @uses idna_convert If available, this will convert an IDN + */ + public function __construct($link = null, $type = null, $length = null, $javascript = null, $bitrate = null, $captions = null, $categories = null, $channels = null, $copyright = null, $credits = null, $description = null, $duration = null, $expression = null, $framerate = null, $hashes = null, $height = null, $keywords = null, $lang = null, $medium = null, $player = null, $ratings = null, $restrictions = null, $samplingrate = null, $thumbnails = null, $title = null, $width = null) + { + $this->bitrate = $bitrate; + $this->captions = $captions; + $this->categories = $categories; + $this->channels = $channels; + $this->copyright = $copyright; + $this->credits = $credits; + $this->description = $description; + $this->duration = $duration; + $this->expression = $expression; + $this->framerate = $framerate; + $this->hashes = $hashes; + $this->height = $height; + $this->keywords = $keywords; + $this->lang = $lang; + $this->length = $length; + $this->link = $link; + $this->medium = $medium; + $this->player = $player; + $this->ratings = $ratings; + $this->restrictions = $restrictions; + $this->samplingrate = $samplingrate; + $this->thumbnails = $thumbnails; + $this->title = $title; + $this->type = $type; + $this->width = $width; + + if (class_exists('idna_convert')) + { + $idn = new idna_convert(); + $parsed = SimplePie_Misc::parse_url($link); + $this->link = SimplePie_Misc::compress_parse_url($parsed['scheme'], $idn->encode($parsed['authority']), $parsed['path'], $parsed['query'], $parsed['fragment']); + } + $this->handler = $this->get_handler(); // Needs to load last + } + + /** + * String-ified version + * + * @return string + */ + public function __toString() + { + // There is no $this->data here + return md5(serialize($this)); + } + + /** + * Get the bitrate + * + * @return string|null + */ + public function get_bitrate() + { + if ($this->bitrate !== null) + { + return $this->bitrate; + } + else + { + return null; + } + } + + /** + * Get a single caption + * + * @param int $key + * @return SimplePie_Caption|null + */ + public function get_caption($key = 0) + { + $captions = $this->get_captions(); + if (isset($captions[$key])) + { + return $captions[$key]; + } + else + { + return null; + } + } + + /** + * Get all captions + * + * @return array|null Array of {@see SimplePie_Caption} objects + */ + public function get_captions() + { + if ($this->captions !== null) + { + return $this->captions; + } + else + { + return null; + } + } + + /** + * Get a single category + * + * @param int $key + * @return SimplePie_Category|null + */ + public function get_category($key = 0) + { + $categories = $this->get_categories(); + if (isset($categories[$key])) + { + return $categories[$key]; + } + else + { + return null; + } + } + + /** + * Get all categories + * + * @return array|null Array of {@see SimplePie_Category} objects + */ + public function get_categories() + { + if ($this->categories !== null) + { + return $this->categories; + } + else + { + return null; + } + } + + /** + * Get the number of audio channels + * + * @return int|null + */ + public function get_channels() + { + if ($this->channels !== null) + { + return $this->channels; + } + else + { + return null; + } + } + + /** + * Get the copyright information + * + * @return SimplePie_Copyright|null + */ + public function get_copyright() + { + if ($this->copyright !== null) + { + return $this->copyright; + } + else + { + return null; + } + } + + /** + * Get a single credit + * + * @param int $key + * @return SimplePie_Credit|null + */ + public function get_credit($key = 0) + { + $credits = $this->get_credits(); + if (isset($credits[$key])) + { + return $credits[$key]; + } + else + { + return null; + } + } + + /** + * Get all credits + * + * @return array|null Array of {@see SimplePie_Credit} objects + */ + public function get_credits() + { + if ($this->credits !== null) + { + return $this->credits; + } + else + { + return null; + } + } + + /** + * Get the description of the enclosure + * + * @return string|null + */ + public function get_description() + { + if ($this->description !== null) + { + return $this->description; + } + else + { + return null; + } + } + + /** + * Get the duration of the enclosure + * + * @param bool $convert Convert seconds into hh:mm:ss + * @return string|int|null 'hh:mm:ss' string if `$convert` was specified, otherwise integer (or null if none found) + */ + public function get_duration($convert = false) + { + if ($this->duration !== null) + { + if ($convert) + { + $time = SimplePie_Misc::time_hms($this->duration); + return $time; + } + else + { + return $this->duration; + } + } + else + { + return null; + } + } + + /** + * Get the expression + * + * @return string Probably one of 'sample', 'full', 'nonstop', 'clip'. Defaults to 'full' + */ + public function get_expression() + { + if ($this->expression !== null) + { + return $this->expression; + } + else + { + return 'full'; + } + } + + /** + * Get the file extension + * + * @return string|null + */ + public function get_extension() + { + if ($this->link !== null) + { + $url = SimplePie_Misc::parse_url($this->link); + if ($url['path'] !== '') + { + return pathinfo($url['path'], PATHINFO_EXTENSION); + } + } + return null; + } + + /** + * Get the framerate (in frames-per-second) + * + * @return string|null + */ + public function get_framerate() + { + if ($this->framerate !== null) + { + return $this->framerate; + } + else + { + return null; + } + } + + /** + * Get the preferred handler + * + * @return string|null One of 'flash', 'fmedia', 'quicktime', 'wmedia', 'mp3' + */ + public function get_handler() + { + return $this->get_real_type(true); + } + + /** + * Get a single hash + * + * @link http://www.rssboard.org/media-rss#media-hash + * @param int $key + * @return string|null Hash as per `media:hash`, prefixed with "$algo:" + */ + public function get_hash($key = 0) + { + $hashes = $this->get_hashes(); + if (isset($hashes[$key])) + { + return $hashes[$key]; + } + else + { + return null; + } + } + + /** + * Get all credits + * + * @return array|null Array of strings, see {@see get_hash()} + */ + public function get_hashes() + { + if ($this->hashes !== null) + { + return $this->hashes; + } + else + { + return null; + } + } + + /** + * Get the height + * + * @return string|null + */ + public function get_height() + { + if ($this->height !== null) + { + return $this->height; + } + else + { + return null; + } + } + + /** + * Get the language + * + * @link http://tools.ietf.org/html/rfc3066 + * @return string|null Language code as per RFC 3066 + */ + public function get_language() + { + if ($this->lang !== null) + { + return $this->lang; + } + else + { + return null; + } + } + + /** + * Get a single keyword + * + * @param int $key + * @return string|null + */ + public function get_keyword($key = 0) + { + $keywords = $this->get_keywords(); + if (isset($keywords[$key])) + { + return $keywords[$key]; + } + else + { + return null; + } + } + + /** + * Get all keywords + * + * @return array|null Array of strings + */ + public function get_keywords() + { + if ($this->keywords !== null) + { + return $this->keywords; + } + else + { + return null; + } + } + + /** + * Get length + * + * @return float Length in bytes + */ + public function get_length() + { + if ($this->length !== null) + { + return $this->length; + } + else + { + return null; + } + } + + /** + * Get the URL + * + * @return string|null + */ + public function get_link() + { + if ($this->link !== null) + { + return urldecode($this->link); + } + else + { + return null; + } + } + + /** + * Get the medium + * + * @link http://www.rssboard.org/media-rss#media-content + * @return string|null Should be one of 'image', 'audio', 'video', 'document', 'executable' + */ + public function get_medium() + { + if ($this->medium !== null) + { + return $this->medium; + } + else + { + return null; + } + } + + /** + * Get the player URL + * + * Typically the same as {@see get_permalink()} + * @return string|null Player URL + */ + public function get_player() + { + if ($this->player !== null) + { + return $this->player; + } + else + { + return null; + } + } + + /** + * Get a single rating + * + * @param int $key + * @return SimplePie_Rating|null + */ + public function get_rating($key = 0) + { + $ratings = $this->get_ratings(); + if (isset($ratings[$key])) + { + return $ratings[$key]; + } + else + { + return null; + } + } + + /** + * Get all ratings + * + * @return array|null Array of {@see SimplePie_Rating} objects + */ + public function get_ratings() + { + if ($this->ratings !== null) + { + return $this->ratings; + } + else + { + return null; + } + } + + /** + * Get a single restriction + * + * @param int $key + * @return SimplePie_Restriction|null + */ + public function get_restriction($key = 0) + { + $restrictions = $this->get_restrictions(); + if (isset($restrictions[$key])) + { + return $restrictions[$key]; + } + else + { + return null; + } + } + + /** + * Get all restrictions + * + * @return array|null Array of {@see SimplePie_Restriction} objects + */ + public function get_restrictions() + { + if ($this->restrictions !== null) + { + return $this->restrictions; + } + else + { + return null; + } + } + + /** + * Get the sampling rate (in kHz) + * + * @return string|null + */ + public function get_sampling_rate() + { + if ($this->samplingrate !== null) + { + return $this->samplingrate; + } + else + { + return null; + } + } + + /** + * Get the file size (in MiB) + * + * @return float|null File size in mebibytes (1048 bytes) + */ + public function get_size() + { + $length = $this->get_length(); + if ($length !== null) + { + return round($length/1048576, 2); + } + else + { + return null; + } + } + + /** + * Get a single thumbnail + * + * @param int $key + * @return string|null Thumbnail URL + */ + public function get_thumbnail($key = 0) + { + $thumbnails = $this->get_thumbnails(); + if (isset($thumbnails[$key])) + { + return $thumbnails[$key]; + } + else + { + return null; + } + } + + /** + * Get all thumbnails + * + * @return array|null Array of thumbnail URLs + */ + public function get_thumbnails() + { + if ($this->thumbnails !== null) + { + return $this->thumbnails; + } + else + { + return null; + } + } + + /** + * Get the title + * + * @return string|null + */ + public function get_title() + { + if ($this->title !== null) + { + return $this->title; + } + else + { + return null; + } + } + + /** + * Get mimetype of the enclosure + * + * @see get_real_type() + * @return string|null MIME type + */ + public function get_type() + { + if ($this->type !== null) + { + return $this->type; + } + else + { + return null; + } + } + + /** + * Get the width + * + * @return string|null + */ + public function get_width() + { + if ($this->width !== null) + { + return $this->width; + } + else + { + return null; + } + } + + /** + * Embed the enclosure using `<embed>` + * + * @deprecated Use the second parameter to {@see embed} instead + * + * @param array|string $options See first paramter to {@see embed} + * @return string HTML string to output + */ + public function native_embed($options='') + { + return $this->embed($options, true); + } + + /** + * Embed the enclosure using Javascript + * + * `$options` is an array or comma-separated key:value string, with the + * following properties: + * + * - `alt` (string): Alternate content for when an end-user does not have + * the appropriate handler installed or when a file type is + * unsupported. Can be any text or HTML. Defaults to blank. + * - `altclass` (string): If a file type is unsupported, the end-user will + * see the alt text (above) linked directly to the content. That link + * will have this value as its class name. Defaults to blank. + * - `audio` (string): This is an image that should be used as a + * placeholder for audio files before they're loaded (QuickTime-only). + * Can be any relative or absolute URL. Defaults to blank. + * - `bgcolor` (string): The background color for the media, if not + * already transparent. Defaults to `#ffffff`. + * - `height` (integer): The height of the embedded media. Accepts any + * numeric pixel value (such as `360`) or `auto`. Defaults to `auto`, + * and it is recommended that you use this default. + * - `loop` (boolean): Do you want the media to loop when it's done? + * Defaults to `false`. + * - `mediaplayer` (string): The location of the included + * `mediaplayer.swf` file. This allows for the playback of Flash Video + * (`.flv`) files, and is the default handler for non-Odeo MP3's. + * Defaults to blank. + * - `video` (string): This is an image that should be used as a + * placeholder for video files before they're loaded (QuickTime-only). + * Can be any relative or absolute URL. Defaults to blank. + * - `width` (integer): The width of the embedded media. Accepts any + * numeric pixel value (such as `480`) or `auto`. Defaults to `auto`, + * and it is recommended that you use this default. + * - `widescreen` (boolean): Is the enclosure widescreen or standard? + * This applies only to video enclosures, and will automatically resize + * the content appropriately. Defaults to `false`, implying 4:3 mode. + * + * Note: Non-widescreen (4:3) mode with `width` and `height` set to `auto` + * will default to 480x360 video resolution. Widescreen (16:9) mode with + * `width` and `height` set to `auto` will default to 480x270 video resolution. + * + * @todo If the dimensions for media:content are defined, use them when width/height are set to 'auto'. + * @param array|string $options Comma-separated key:value list, or array + * @param bool $native Use `<embed>` + * @return string HTML string to output + */ + public function embed($options = '', $native = false) + { + // Set up defaults + $audio = ''; + $video = ''; + $alt = ''; + $altclass = ''; + $loop = 'false'; + $width = 'auto'; + $height = 'auto'; + $bgcolor = '#ffffff'; + $mediaplayer = ''; + $widescreen = false; + $handler = $this->get_handler(); + $type = $this->get_real_type(); + + // Process options and reassign values as necessary + if (is_array($options)) + { + extract($options); + } + else + { + $options = explode(',', $options); + foreach($options as $option) + { + $opt = explode(':', $option, 2); + if (isset($opt[0], $opt[1])) + { + $opt[0] = trim($opt[0]); + $opt[1] = trim($opt[1]); + switch ($opt[0]) + { + case 'audio': + $audio = $opt[1]; + break; + + case 'video': + $video = $opt[1]; + break; + + case 'alt': + $alt = $opt[1]; + break; + + case 'altclass': + $altclass = $opt[1]; + break; + + case 'loop': + $loop = $opt[1]; + break; + + case 'width': + $width = $opt[1]; + break; + + case 'height': + $height = $opt[1]; + break; + + case 'bgcolor': + $bgcolor = $opt[1]; + break; + + case 'mediaplayer': + $mediaplayer = $opt[1]; + break; + + case 'widescreen': + $widescreen = $opt[1]; + break; + } + } + } + } + + $mime = explode('/', $type, 2); + $mime = $mime[0]; + + // Process values for 'auto' + if ($width === 'auto') + { + if ($mime === 'video') + { + if ($height === 'auto') + { + $width = 480; + } + elseif ($widescreen) + { + $width = round((intval($height)/9)*16); + } + else + { + $width = round((intval($height)/3)*4); + } + } + else + { + $width = '100%'; + } + } + + if ($height === 'auto') + { + if ($mime === 'audio') + { + $height = 0; + } + elseif ($mime === 'video') + { + if ($width === 'auto') + { + if ($widescreen) + { + $height = 270; + } + else + { + $height = 360; + } + } + elseif ($widescreen) + { + $height = round((intval($width)/16)*9); + } + else + { + $height = round((intval($width)/4)*3); + } + } + else + { + $height = 376; + } + } + elseif ($mime === 'audio') + { + $height = 0; + } + + // Set proper placeholder value + if ($mime === 'audio') + { + $placeholder = $audio; + } + elseif ($mime === 'video') + { + $placeholder = $video; + } + + $embed = ''; + + // Flash + if ($handler === 'flash') + { + if ($native) + { + $embed .= "<embed src=\"" . $this->get_link() . "\" pluginspage=\"http://adobe.com/go/getflashplayer\" type=\"$type\" quality=\"high\" width=\"$width\" height=\"$height\" bgcolor=\"$bgcolor\" loop=\"$loop\"></embed>"; + } + else + { + $embed .= "<script type='text/javascript'>embed_flash('$bgcolor', '$width', '$height', '" . $this->get_link() . "', '$loop', '$type');</script>"; + } + } + + // Flash Media Player file types. + // Preferred handler for MP3 file types. + elseif ($handler === 'fmedia' || ($handler === 'mp3' && $mediaplayer !== '')) + { + $height += 20; + if ($native) + { + $embed .= "<embed src=\"$mediaplayer\" pluginspage=\"http://adobe.com/go/getflashplayer\" type=\"application/x-shockwave-flash\" quality=\"high\" width=\"$width\" height=\"$height\" wmode=\"transparent\" flashvars=\"file=" . rawurlencode($this->get_link().'?file_extension=.'.$this->get_extension()) . "&autostart=false&repeat=$loop&showdigits=true&showfsbutton=false\"></embed>"; + } + else + { + $embed .= "<script type='text/javascript'>embed_flv('$width', '$height', '" . rawurlencode($this->get_link().'?file_extension=.'.$this->get_extension()) . "', '$placeholder', '$loop', '$mediaplayer');</script>"; + } + } + + // QuickTime 7 file types. Need to test with QuickTime 6. + // Only handle MP3's if the Flash Media Player is not present. + elseif ($handler === 'quicktime' || ($handler === 'mp3' && $mediaplayer === '')) + { + $height += 16; + if ($native) + { + if ($placeholder !== '') + { + $embed .= "<embed type=\"$type\" style=\"cursor:hand; cursor:pointer;\" href=\"" . $this->get_link() . "\" src=\"$placeholder\" width=\"$width\" height=\"$height\" autoplay=\"false\" target=\"myself\" controller=\"false\" loop=\"$loop\" scale=\"aspect\" bgcolor=\"$bgcolor\" pluginspage=\"http://apple.com/quicktime/download/\"></embed>"; + } + else + { + $embed .= "<embed type=\"$type\" style=\"cursor:hand; cursor:pointer;\" src=\"" . $this->get_link() . "\" width=\"$width\" height=\"$height\" autoplay=\"false\" target=\"myself\" controller=\"true\" loop=\"$loop\" scale=\"aspect\" bgcolor=\"$bgcolor\" pluginspage=\"http://apple.com/quicktime/download/\"></embed>"; + } + } + else + { + $embed .= "<script type='text/javascript'>embed_quicktime('$type', '$bgcolor', '$width', '$height', '" . $this->get_link() . "', '$placeholder', '$loop');</script>"; + } + } + + // Windows Media + elseif ($handler === 'wmedia') + { + $height += 45; + if ($native) + { + $embed .= "<embed type=\"application/x-mplayer2\" src=\"" . $this->get_link() . "\" autosize=\"1\" width=\"$width\" height=\"$height\" showcontrols=\"1\" showstatusbar=\"0\" showdisplay=\"0\" autostart=\"0\"></embed>"; + } + else + { + $embed .= "<script type='text/javascript'>embed_wmedia('$width', '$height', '" . $this->get_link() . "');</script>"; + } + } + + // Everything else + else $embed .= '<a href="' . $this->get_link() . '" class="' . $altclass . '">' . $alt . '</a>'; + + return $embed; + } + + /** + * Get the real media type + * + * Often, feeds lie to us, necessitating a bit of deeper inspection. This + * converts types to their canonical representations based on the file + * extension + * + * @see get_type() + * @param bool $find_handler Internal use only, use {@see get_handler()} instead + * @return string MIME type + */ + public function get_real_type($find_handler = false) + { + // Mime-types by handler. + $types_flash = array('application/x-shockwave-flash', 'application/futuresplash'); // Flash + $types_fmedia = array('video/flv', 'video/x-flv','flv-application/octet-stream'); // Flash Media Player + $types_quicktime = array('audio/3gpp', 'audio/3gpp2', 'audio/aac', 'audio/x-aac', 'audio/aiff', 'audio/x-aiff', 'audio/mid', 'audio/midi', 'audio/x-midi', 'audio/mp4', 'audio/m4a', 'audio/x-m4a', 'audio/wav', 'audio/x-wav', 'video/3gpp', 'video/3gpp2', 'video/m4v', 'video/x-m4v', 'video/mp4', 'video/mpeg', 'video/x-mpeg', 'video/quicktime', 'video/sd-video'); // QuickTime + $types_wmedia = array('application/asx', 'application/x-mplayer2', 'audio/x-ms-wma', 'audio/x-ms-wax', 'video/x-ms-asf-plugin', 'video/x-ms-asf', 'video/x-ms-wm', 'video/x-ms-wmv', 'video/x-ms-wvx'); // Windows Media + $types_mp3 = array('audio/mp3', 'audio/x-mp3', 'audio/mpeg', 'audio/x-mpeg'); // MP3 + + if ($this->get_type() !== null) + { + $type = strtolower($this->type); + } + else + { + $type = null; + } + + // If we encounter an unsupported mime-type, check the file extension and guess intelligently. + if (!in_array($type, array_merge($types_flash, $types_fmedia, $types_quicktime, $types_wmedia, $types_mp3))) + { + switch (strtolower($this->get_extension())) + { + // Audio mime-types + case 'aac': + case 'adts': + $type = 'audio/acc'; + break; + + case 'aif': + case 'aifc': + case 'aiff': + case 'cdda': + $type = 'audio/aiff'; + break; + + case 'bwf': + $type = 'audio/wav'; + break; + + case 'kar': + case 'mid': + case 'midi': + case 'smf': + $type = 'audio/midi'; + break; + + case 'm4a': + $type = 'audio/x-m4a'; + break; + + case 'mp3': + case 'swa': + $type = 'audio/mp3'; + break; + + case 'wav': + $type = 'audio/wav'; + break; + + case 'wax': + $type = 'audio/x-ms-wax'; + break; + + case 'wma': + $type = 'audio/x-ms-wma'; + break; + + // Video mime-types + case '3gp': + case '3gpp': + $type = 'video/3gpp'; + break; + + case '3g2': + case '3gp2': + $type = 'video/3gpp2'; + break; + + case 'asf': + $type = 'video/x-ms-asf'; + break; + + case 'flv': + $type = 'video/x-flv'; + break; + + case 'm1a': + case 'm1s': + case 'm1v': + case 'm15': + case 'm75': + case 'mp2': + case 'mpa': + case 'mpeg': + case 'mpg': + case 'mpm': + case 'mpv': + $type = 'video/mpeg'; + break; + + case 'm4v': + $type = 'video/x-m4v'; + break; + + case 'mov': + case 'qt': + $type = 'video/quicktime'; + break; + + case 'mp4': + case 'mpg4': + $type = 'video/mp4'; + break; + + case 'sdv': + $type = 'video/sd-video'; + break; + + case 'wm': + $type = 'video/x-ms-wm'; + break; + + case 'wmv': + $type = 'video/x-ms-wmv'; + break; + + case 'wvx': + $type = 'video/x-ms-wvx'; + break; + + // Flash mime-types + case 'spl': + $type = 'application/futuresplash'; + break; + + case 'swf': + $type = 'application/x-shockwave-flash'; + break; + } + } + + if ($find_handler) + { + if (in_array($type, $types_flash)) + { + return 'flash'; + } + elseif (in_array($type, $types_fmedia)) + { + return 'fmedia'; + } + elseif (in_array($type, $types_quicktime)) + { + return 'quicktime'; + } + elseif (in_array($type, $types_wmedia)) + { + return 'wmedia'; + } + elseif (in_array($type, $types_mp3)) + { + return 'mp3'; + } + else + { + return null; + } + } + else + { + return $type; + } + } +} + diff --git a/vendor/simplepie/simplepie/library/SimplePie/Exception.php b/vendor/simplepie/simplepie/library/SimplePie/Exception.php new file mode 100644 index 000000000..53c015e77 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Exception.php @@ -0,0 +1,51 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * General SimplePie exception class + * + * @package SimplePie + */ +class SimplePie_Exception extends Exception +{ +}
\ No newline at end of file diff --git a/vendor/simplepie/simplepie/library/SimplePie/File.php b/vendor/simplepie/simplepie/library/SimplePie/File.php new file mode 100644 index 000000000..e670e05a0 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/File.php @@ -0,0 +1,306 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Used for fetching remote files and reading local files + * + * Supports HTTP 1.0 via cURL or fsockopen, with spotty HTTP 1.1 support + * + * This class can be overloaded with {@see SimplePie::set_file_class()} + * + * @package SimplePie + * @subpackage HTTP + * @todo Move to properly supporting RFC2616 (HTTP/1.1) + */ +class SimplePie_File +{ + var $url; + var $useragent; + var $success = true; + var $headers = array(); + var $body; + var $status_code; + var $redirects = 0; + var $error; + var $method = SIMPLEPIE_FILE_SOURCE_NONE; + var $permanent_url; + + public function __construct($url, $timeout = 10, $redirects = 5, $headers = null, $useragent = null, $force_fsockopen = false, $curl_options = array()) + { + if (class_exists('idna_convert')) + { + $idn = new idna_convert(); + $parsed = SimplePie_Misc::parse_url($url); + $url = SimplePie_Misc::compress_parse_url($parsed['scheme'], $idn->encode($parsed['authority']), $parsed['path'], $parsed['query'], $parsed['fragment']); + } + $this->url = $url; + $this->permanent_url = $url; + $this->useragent = $useragent; + if (preg_match('/^http(s)?:\/\//i', $url)) + { + if ($useragent === null) + { + $useragent = ini_get('user_agent'); + $this->useragent = $useragent; + } + if (!is_array($headers)) + { + $headers = array(); + } + if (!$force_fsockopen && function_exists('curl_exec')) + { + $this->method = SIMPLEPIE_FILE_SOURCE_REMOTE | SIMPLEPIE_FILE_SOURCE_CURL; + $fp = curl_init(); + $headers2 = array(); + foreach ($headers as $key => $value) + { + $headers2[] = "$key: $value"; + } + if (version_compare(SimplePie_Misc::get_curl_version(), '7.10.5', '>=')) + { + curl_setopt($fp, CURLOPT_ENCODING, ''); + } + curl_setopt($fp, CURLOPT_URL, $url); + curl_setopt($fp, CURLOPT_HEADER, 1); + curl_setopt($fp, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($fp, CURLOPT_FAILONERROR, 1); + curl_setopt($fp, CURLOPT_TIMEOUT, $timeout); + curl_setopt($fp, CURLOPT_CONNECTTIMEOUT, $timeout); + curl_setopt($fp, CURLOPT_REFERER, $url); + curl_setopt($fp, CURLOPT_USERAGENT, $useragent); + curl_setopt($fp, CURLOPT_HTTPHEADER, $headers2); + if (!ini_get('open_basedir') && !ini_get('safe_mode') && version_compare(SimplePie_Misc::get_curl_version(), '7.15.2', '>=')) + { + curl_setopt($fp, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($fp, CURLOPT_MAXREDIRS, $redirects); + } + foreach ($curl_options as $curl_param => $curl_value) { + curl_setopt($fp, $curl_param, $curl_value); + } + + $this->headers = curl_exec($fp); + if (curl_errno($fp) === 23 || curl_errno($fp) === 61) + { + curl_setopt($fp, CURLOPT_ENCODING, 'none'); + $this->headers = curl_exec($fp); + } + if (curl_errno($fp)) + { + $this->error = 'cURL error ' . curl_errno($fp) . ': ' . curl_error($fp); + $this->success = false; + } + else + { + // Use the updated url provided by curl_getinfo after any redirects. + if ($info = curl_getinfo($fp)) { + $this->url = $info['url']; + } + curl_close($fp); + $this->headers = explode("\r\n\r\n", $this->headers, $info['redirect_count'] + 1); + $this->headers = array_pop($this->headers); + $parser = new SimplePie_HTTP_Parser($this->headers); + if ($parser->parse()) + { + $this->headers = $parser->headers; + $this->body = trim($parser->body); + $this->status_code = $parser->status_code; + if ((in_array($this->status_code, array(300, 301, 302, 303, 307)) || $this->status_code > 307 && $this->status_code < 400) && isset($this->headers['location']) && $this->redirects < $redirects) + { + $this->redirects++; + $location = SimplePie_Misc::absolutize_url($this->headers['location'], $url); + $previousStatusCode = $this->status_code; + $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen); + $this->permanent_url = ($previousStatusCode == 301) ? $location : $url; + return; + } + } + } + } + else + { + $this->method = SIMPLEPIE_FILE_SOURCE_REMOTE | SIMPLEPIE_FILE_SOURCE_FSOCKOPEN; + $url_parts = parse_url($url); + $socket_host = $url_parts['host']; + if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') + { + $socket_host = "ssl://$url_parts[host]"; + $url_parts['port'] = 443; + } + if (!isset($url_parts['port'])) + { + $url_parts['port'] = 80; + } + $fp = @fsockopen($socket_host, $url_parts['port'], $errno, $errstr, $timeout); + if (!$fp) + { + $this->error = 'fsockopen error: ' . $errstr; + $this->success = false; + } + else + { + stream_set_timeout($fp, $timeout); + if (isset($url_parts['path'])) + { + if (isset($url_parts['query'])) + { + $get = "$url_parts[path]?$url_parts[query]"; + } + else + { + $get = $url_parts['path']; + } + } + else + { + $get = '/'; + } + $out = "GET $get HTTP/1.1\r\n"; + $out .= "Host: $url_parts[host]\r\n"; + $out .= "User-Agent: $useragent\r\n"; + if (extension_loaded('zlib')) + { + $out .= "Accept-Encoding: x-gzip,gzip,deflate\r\n"; + } + + if (isset($url_parts['user']) && isset($url_parts['pass'])) + { + $out .= "Authorization: Basic " . base64_encode("$url_parts[user]:$url_parts[pass]") . "\r\n"; + } + foreach ($headers as $key => $value) + { + $out .= "$key: $value\r\n"; + } + $out .= "Connection: Close\r\n\r\n"; + fwrite($fp, $out); + + $info = stream_get_meta_data($fp); + + $this->headers = ''; + while (!$info['eof'] && !$info['timed_out']) + { + $this->headers .= fread($fp, 1160); + $info = stream_get_meta_data($fp); + } + if (!$info['timed_out']) + { + $parser = new SimplePie_HTTP_Parser($this->headers); + if ($parser->parse()) + { + $this->headers = $parser->headers; + $this->body = $parser->body; + $this->status_code = $parser->status_code; + if ((in_array($this->status_code, array(300, 301, 302, 303, 307)) || $this->status_code > 307 && $this->status_code < 400) && isset($this->headers['location']) && $this->redirects < $redirects) + { + $this->redirects++; + $location = SimplePie_Misc::absolutize_url($this->headers['location'], $url); + $previousStatusCode = $this->status_code; + $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen); + $this->permanent_url = ($previousStatusCode == 301) ? $location : $url; + return; + } + if (isset($this->headers['content-encoding'])) + { + // Hey, we act dumb elsewhere, so let's do that here too + switch (strtolower(trim($this->headers['content-encoding'], "\x09\x0A\x0D\x20"))) + { + case 'gzip': + case 'x-gzip': + $decoder = new SimplePie_gzdecode($this->body); + if (!$decoder->parse()) + { + $this->error = 'Unable to decode HTTP "gzip" stream'; + $this->success = false; + } + else + { + $this->body = trim($decoder->data); + } + break; + + case 'deflate': + if (($decompressed = gzinflate($this->body)) !== false) + { + $this->body = $decompressed; + } + else if (($decompressed = gzuncompress($this->body)) !== false) + { + $this->body = $decompressed; + } + else if (function_exists('gzdecode') && ($decompressed = gzdecode($this->body)) !== false) + { + $this->body = $decompressed; + } + else + { + $this->error = 'Unable to decode HTTP "deflate" stream'; + $this->success = false; + } + break; + + default: + $this->error = 'Unknown content coding'; + $this->success = false; + } + } + } + } + else + { + $this->error = 'fsocket timed out'; + $this->success = false; + } + fclose($fp); + } + } + } + else + { + $this->method = SIMPLEPIE_FILE_SOURCE_LOCAL | SIMPLEPIE_FILE_SOURCE_FILE_GET_CONTENTS; + if (empty($url) || !($this->body = trim(file_get_contents($url)))) + { + $this->error = 'file_get_contents could not read the file'; + $this->success = false; + } + } + } +} diff --git a/vendor/simplepie/simplepie/library/SimplePie/HTTP/Parser.php b/vendor/simplepie/simplepie/library/SimplePie/HTTP/Parser.php new file mode 100644 index 000000000..63ae1e03d --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/HTTP/Parser.php @@ -0,0 +1,499 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + + +/** + * HTTP Response Parser + * + * @package SimplePie + * @subpackage HTTP + */ +class SimplePie_HTTP_Parser +{ + /** + * HTTP Version + * + * @var float + */ + public $http_version = 0.0; + + /** + * Status code + * + * @var int + */ + public $status_code = 0; + + /** + * Reason phrase + * + * @var string + */ + public $reason = ''; + + /** + * Key/value pairs of the headers + * + * @var array + */ + public $headers = array(); + + /** + * Body of the response + * + * @var string + */ + public $body = ''; + + /** + * Current state of the state machine + * + * @var string + */ + protected $state = 'http_version'; + + /** + * Input data + * + * @var string + */ + protected $data = ''; + + /** + * Input data length (to avoid calling strlen() everytime this is needed) + * + * @var int + */ + protected $data_length = 0; + + /** + * Current position of the pointer + * + * @var int + */ + protected $position = 0; + + /** + * Name of the hedaer currently being parsed + * + * @var string + */ + protected $name = ''; + + /** + * Value of the hedaer currently being parsed + * + * @var string + */ + protected $value = ''; + + /** + * Create an instance of the class with the input data + * + * @param string $data Input data + */ + public function __construct($data) + { + $this->data = $data; + $this->data_length = strlen($this->data); + } + + /** + * Parse the input data + * + * @return bool true on success, false on failure + */ + public function parse() + { + while ($this->state && $this->state !== 'emit' && $this->has_data()) + { + $state = $this->state; + $this->$state(); + } + $this->data = ''; + if ($this->state === 'emit' || $this->state === 'body') + { + return true; + } + else + { + $this->http_version = ''; + $this->status_code = ''; + $this->reason = ''; + $this->headers = array(); + $this->body = ''; + return false; + } + } + + /** + * Check whether there is data beyond the pointer + * + * @return bool true if there is further data, false if not + */ + protected function has_data() + { + return (bool) ($this->position < $this->data_length); + } + + /** + * See if the next character is LWS + * + * @return bool true if the next character is LWS, false if not + */ + protected function is_linear_whitespace() + { + return (bool) ($this->data[$this->position] === "\x09" + || $this->data[$this->position] === "\x20" + || ($this->data[$this->position] === "\x0A" + && isset($this->data[$this->position + 1]) + && ($this->data[$this->position + 1] === "\x09" || $this->data[$this->position + 1] === "\x20"))); + } + + /** + * Parse the HTTP version + */ + protected function http_version() + { + if (strpos($this->data, "\x0A") !== false && strtoupper(substr($this->data, 0, 5)) === 'HTTP/') + { + $len = strspn($this->data, '0123456789.', 5); + $this->http_version = substr($this->data, 5, $len); + $this->position += 5 + $len; + if (substr_count($this->http_version, '.') <= 1) + { + $this->http_version = (float) $this->http_version; + $this->position += strspn($this->data, "\x09\x20", $this->position); + $this->state = 'status'; + } + else + { + $this->state = false; + } + } + else + { + $this->state = false; + } + } + + /** + * Parse the status code + */ + protected function status() + { + if ($len = strspn($this->data, '0123456789', $this->position)) + { + $this->status_code = (int) substr($this->data, $this->position, $len); + $this->position += $len; + $this->state = 'reason'; + } + else + { + $this->state = false; + } + } + + /** + * Parse the reason phrase + */ + protected function reason() + { + $len = strcspn($this->data, "\x0A", $this->position); + $this->reason = trim(substr($this->data, $this->position, $len), "\x09\x0D\x20"); + $this->position += $len + 1; + $this->state = 'new_line'; + } + + /** + * Deal with a new line, shifting data around as needed + */ + protected function new_line() + { + $this->value = trim($this->value, "\x0D\x20"); + if ($this->name !== '' && $this->value !== '') + { + $this->name = strtolower($this->name); + // We should only use the last Content-Type header. c.f. issue #1 + if (isset($this->headers[$this->name]) && $this->name !== 'content-type') + { + $this->headers[$this->name] .= ', ' . $this->value; + } + else + { + $this->headers[$this->name] = $this->value; + } + } + $this->name = ''; + $this->value = ''; + if (substr($this->data[$this->position], 0, 2) === "\x0D\x0A") + { + $this->position += 2; + $this->state = 'body'; + } + elseif ($this->data[$this->position] === "\x0A") + { + $this->position++; + $this->state = 'body'; + } + else + { + $this->state = 'name'; + } + } + + /** + * Parse a header name + */ + protected function name() + { + $len = strcspn($this->data, "\x0A:", $this->position); + if (isset($this->data[$this->position + $len])) + { + if ($this->data[$this->position + $len] === "\x0A") + { + $this->position += $len; + $this->state = 'new_line'; + } + else + { + $this->name = substr($this->data, $this->position, $len); + $this->position += $len + 1; + $this->state = 'value'; + } + } + else + { + $this->state = false; + } + } + + /** + * Parse LWS, replacing consecutive LWS characters with a single space + */ + protected function linear_whitespace() + { + do + { + if (substr($this->data, $this->position, 2) === "\x0D\x0A") + { + $this->position += 2; + } + elseif ($this->data[$this->position] === "\x0A") + { + $this->position++; + } + $this->position += strspn($this->data, "\x09\x20", $this->position); + } while ($this->has_data() && $this->is_linear_whitespace()); + $this->value .= "\x20"; + } + + /** + * See what state to move to while within non-quoted header values + */ + protected function value() + { + if ($this->is_linear_whitespace()) + { + $this->linear_whitespace(); + } + else + { + switch ($this->data[$this->position]) + { + case '"': + // Workaround for ETags: we have to include the quotes as + // part of the tag. + if (strtolower($this->name) === 'etag') + { + $this->value .= '"'; + $this->position++; + $this->state = 'value_char'; + break; + } + $this->position++; + $this->state = 'quote'; + break; + + case "\x0A": + $this->position++; + $this->state = 'new_line'; + break; + + default: + $this->state = 'value_char'; + break; + } + } + } + + /** + * Parse a header value while outside quotes + */ + protected function value_char() + { + $len = strcspn($this->data, "\x09\x20\x0A\"", $this->position); + $this->value .= substr($this->data, $this->position, $len); + $this->position += $len; + $this->state = 'value'; + } + + /** + * See what state to move to while within quoted header values + */ + protected function quote() + { + if ($this->is_linear_whitespace()) + { + $this->linear_whitespace(); + } + else + { + switch ($this->data[$this->position]) + { + case '"': + $this->position++; + $this->state = 'value'; + break; + + case "\x0A": + $this->position++; + $this->state = 'new_line'; + break; + + case '\\': + $this->position++; + $this->state = 'quote_escaped'; + break; + + default: + $this->state = 'quote_char'; + break; + } + } + } + + /** + * Parse a header value while within quotes + */ + protected function quote_char() + { + $len = strcspn($this->data, "\x09\x20\x0A\"\\", $this->position); + $this->value .= substr($this->data, $this->position, $len); + $this->position += $len; + $this->state = 'value'; + } + + /** + * Parse an escaped character within quotes + */ + protected function quote_escaped() + { + $this->value .= $this->data[$this->position]; + $this->position++; + $this->state = 'quote'; + } + + /** + * Parse the body + */ + protected function body() + { + $this->body = substr($this->data, $this->position); + if (!empty($this->headers['transfer-encoding'])) + { + unset($this->headers['transfer-encoding']); + $this->state = 'chunked'; + } + else + { + $this->state = 'emit'; + } + } + + /** + * Parsed a "Transfer-Encoding: chunked" body + */ + protected function chunked() + { + if (!preg_match('/^([0-9a-f]+)[^\r\n]*\r\n/i', trim($this->body))) + { + $this->state = 'emit'; + return; + } + + $decoded = ''; + $encoded = $this->body; + + while (true) + { + $is_chunked = (bool) preg_match( '/^([0-9a-f]+)[^\r\n]*\r\n/i', $encoded, $matches ); + if (!$is_chunked) + { + // Looks like it's not chunked after all + $this->state = 'emit'; + return; + } + + $length = hexdec(trim($matches[1])); + if ($length === 0) + { + // Ignore trailer headers + $this->state = 'emit'; + $this->body = $decoded; + return; + } + + $chunk_length = strlen($matches[0]); + $decoded .= $part = substr($encoded, $chunk_length, $length); + $encoded = substr($encoded, $chunk_length + $length + 2); + + if (trim($encoded) === '0' || empty($encoded)) + { + $this->state = 'emit'; + $this->body = $decoded; + return; + } + } + } +} diff --git a/vendor/simplepie/simplepie/library/SimplePie/IRI.php b/vendor/simplepie/simplepie/library/SimplePie/IRI.php new file mode 100644 index 000000000..2b3fbaf07 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/IRI.php @@ -0,0 +1,1257 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * IRI parser/serialiser/normaliser + * + * @package SimplePie + * @subpackage HTTP + * @author Geoffrey Sneddon + * @author Steve Minutillo + * @author Ryan McCue + * @copyright 2007-2012 Geoffrey Sneddon, Steve Minutillo, Ryan McCue + * @license http://www.opensource.org/licenses/bsd-license.php + */ +class SimplePie_IRI +{ + /** + * Scheme + * + * @var string + */ + protected $scheme = null; + + /** + * User Information + * + * @var string + */ + protected $iuserinfo = null; + + /** + * ihost + * + * @var string + */ + protected $ihost = null; + + /** + * Port + * + * @var string + */ + protected $port = null; + + /** + * ipath + * + * @var string + */ + protected $ipath = ''; + + /** + * iquery + * + * @var string + */ + protected $iquery = null; + + /** + * ifragment + * + * @var string + */ + protected $ifragment = null; + + /** + * Normalization database + * + * Each key is the scheme, each value is an array with each key as the IRI + * part and value as the default value for that part. + */ + protected $normalization = array( + 'acap' => array( + 'port' => 674 + ), + 'dict' => array( + 'port' => 2628 + ), + 'file' => array( + 'ihost' => 'localhost' + ), + 'http' => array( + 'port' => 80, + 'ipath' => '/' + ), + 'https' => array( + 'port' => 443, + 'ipath' => '/' + ), + ); + + /** + * Return the entire IRI when you try and read the object as a string + * + * @return string + */ + public function __toString() + { + return $this->get_iri(); + } + + /** + * Overload __set() to provide access via properties + * + * @param string $name Property name + * @param mixed $value Property value + */ + public function __set($name, $value) + { + if (method_exists($this, 'set_' . $name)) + { + call_user_func(array($this, 'set_' . $name), $value); + } + elseif ( + $name === 'iauthority' + || $name === 'iuserinfo' + || $name === 'ihost' + || $name === 'ipath' + || $name === 'iquery' + || $name === 'ifragment' + ) + { + call_user_func(array($this, 'set_' . substr($name, 1)), $value); + } + } + + /** + * Overload __get() to provide access via properties + * + * @param string $name Property name + * @return mixed + */ + public function __get($name) + { + // isset() returns false for null, we don't want to do that + // Also why we use array_key_exists below instead of isset() + $props = get_object_vars($this); + + if ( + $name === 'iri' || + $name === 'uri' || + $name === 'iauthority' || + $name === 'authority' + ) + { + $return = $this->{"get_$name"}(); + } + elseif (array_key_exists($name, $props)) + { + $return = $this->$name; + } + // host -> ihost + elseif (($prop = 'i' . $name) && array_key_exists($prop, $props)) + { + $name = $prop; + $return = $this->$prop; + } + // ischeme -> scheme + elseif (($prop = substr($name, 1)) && array_key_exists($prop, $props)) + { + $name = $prop; + $return = $this->$prop; + } + else + { + trigger_error('Undefined property: ' . get_class($this) . '::' . $name, E_USER_NOTICE); + $return = null; + } + + if ($return === null && isset($this->normalization[$this->scheme][$name])) + { + return $this->normalization[$this->scheme][$name]; + } + else + { + return $return; + } + } + + /** + * Overload __isset() to provide access via properties + * + * @param string $name Property name + * @return bool + */ + public function __isset($name) + { + if (method_exists($this, 'get_' . $name) || isset($this->$name)) + { + return true; + } + else + { + return false; + } + } + + /** + * Overload __unset() to provide access via properties + * + * @param string $name Property name + */ + public function __unset($name) + { + if (method_exists($this, 'set_' . $name)) + { + call_user_func(array($this, 'set_' . $name), ''); + } + } + + /** + * Create a new IRI object, from a specified string + * + * @param string $iri + */ + public function __construct($iri = null) + { + $this->set_iri($iri); + } + + /** + * Clean up + */ + public function __destruct() { + $this->set_iri(null, true); + $this->set_path(null, true); + $this->set_authority(null, true); + } + + /** + * Create a new IRI object by resolving a relative IRI + * + * Returns false if $base is not absolute, otherwise an IRI. + * + * @param IRI|string $base (Absolute) Base IRI + * @param IRI|string $relative Relative IRI + * @return IRI|false + */ + public static function absolutize($base, $relative) + { + if (!($relative instanceof SimplePie_IRI)) + { + $relative = new SimplePie_IRI($relative); + } + if (!$relative->is_valid()) + { + return false; + } + elseif ($relative->scheme !== null) + { + return clone $relative; + } + else + { + if (!($base instanceof SimplePie_IRI)) + { + $base = new SimplePie_IRI($base); + } + if ($base->scheme !== null && $base->is_valid()) + { + if ($relative->get_iri() !== '') + { + if ($relative->iuserinfo !== null || $relative->ihost !== null || $relative->port !== null) + { + $target = clone $relative; + $target->scheme = $base->scheme; + } + else + { + $target = new SimplePie_IRI; + $target->scheme = $base->scheme; + $target->iuserinfo = $base->iuserinfo; + $target->ihost = $base->ihost; + $target->port = $base->port; + if ($relative->ipath !== '') + { + if ($relative->ipath[0] === '/') + { + $target->ipath = $relative->ipath; + } + elseif (($base->iuserinfo !== null || $base->ihost !== null || $base->port !== null) && $base->ipath === '') + { + $target->ipath = '/' . $relative->ipath; + } + elseif (($last_segment = strrpos($base->ipath, '/')) !== false) + { + $target->ipath = substr($base->ipath, 0, $last_segment + 1) . $relative->ipath; + } + else + { + $target->ipath = $relative->ipath; + } + $target->ipath = $target->remove_dot_segments($target->ipath); + $target->iquery = $relative->iquery; + } + else + { + $target->ipath = $base->ipath; + if ($relative->iquery !== null) + { + $target->iquery = $relative->iquery; + } + elseif ($base->iquery !== null) + { + $target->iquery = $base->iquery; + } + } + $target->ifragment = $relative->ifragment; + } + } + else + { + $target = clone $base; + $target->ifragment = null; + } + $target->scheme_normalization(); + return $target; + } + else + { + return false; + } + } + } + + /** + * Parse an IRI into scheme/authority/path/query/fragment segments + * + * @param string $iri + * @return array + */ + protected function parse_iri($iri) + { + $iri = trim($iri, "\x20\x09\x0A\x0C\x0D"); + if (preg_match('/^((?P<scheme>[^:\/?#]+):)?(\/\/(?P<authority>[^\/?#]*))?(?P<path>[^?#]*)(\?(?P<query>[^#]*))?(#(?P<fragment>.*))?$/', $iri, $match)) + { + if ($match[1] === '') + { + $match['scheme'] = null; + } + if (!isset($match[3]) || $match[3] === '') + { + $match['authority'] = null; + } + if (!isset($match[5])) + { + $match['path'] = ''; + } + if (!isset($match[6]) || $match[6] === '') + { + $match['query'] = null; + } + if (!isset($match[8]) || $match[8] === '') + { + $match['fragment'] = null; + } + return $match; + } + else + { + // This can occur when a paragraph is accidentally parsed as a URI + return false; + } + } + + /** + * Remove dot segments from a path + * + * @param string $input + * @return string + */ + protected function remove_dot_segments($input) + { + $output = ''; + while (strpos($input, './') !== false || strpos($input, '/.') !== false || $input === '.' || $input === '..') + { + // A: If the input buffer begins with a prefix of "../" or "./", then remove that prefix from the input buffer; otherwise, + if (strpos($input, '../') === 0) + { + $input = substr($input, 3); + } + elseif (strpos($input, './') === 0) + { + $input = substr($input, 2); + } + // B: if the input buffer begins with a prefix of "/./" or "/.", where "." is a complete path segment, then replace that prefix with "/" in the input buffer; otherwise, + elseif (strpos($input, '/./') === 0) + { + $input = substr($input, 2); + } + elseif ($input === '/.') + { + $input = '/'; + } + // C: if the input buffer begins with a prefix of "/../" or "/..", where ".." is a complete path segment, then replace that prefix with "/" in the input buffer and remove the last segment and its preceding "/" (if any) from the output buffer; otherwise, + elseif (strpos($input, '/../') === 0) + { + $input = substr($input, 3); + $output = substr_replace($output, '', strrpos($output, '/')); + } + elseif ($input === '/..') + { + $input = '/'; + $output = substr_replace($output, '', strrpos($output, '/')); + } + // D: if the input buffer consists only of "." or "..", then remove that from the input buffer; otherwise, + elseif ($input === '.' || $input === '..') + { + $input = ''; + } + // E: move the first path segment in the input buffer to the end of the output buffer, including the initial "/" character (if any) and any subsequent characters up to, but not including, the next "/" character or the end of the input buffer + elseif (($pos = strpos($input, '/', 1)) !== false) + { + $output .= substr($input, 0, $pos); + $input = substr_replace($input, '', 0, $pos); + } + else + { + $output .= $input; + $input = ''; + } + } + return $output . $input; + } + + /** + * Replace invalid character with percent encoding + * + * @param string $string Input string + * @param string $extra_chars Valid characters not in iunreserved or + * iprivate (this is ASCII-only) + * @param bool $iprivate Allow iprivate + * @return string + */ + protected function replace_invalid_with_pct_encoding($string, $extra_chars, $iprivate = false) + { + // Normalize as many pct-encoded sections as possible + $string = preg_replace_callback('/(?:%[A-Fa-f0-9]{2})+/', array($this, 'remove_iunreserved_percent_encoded'), $string); + + // Replace invalid percent characters + $string = preg_replace('/%(?![A-Fa-f0-9]{2})/', '%25', $string); + + // Add unreserved and % to $extra_chars (the latter is safe because all + // pct-encoded sections are now valid). + $extra_chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~%'; + + // Now replace any bytes that aren't allowed with their pct-encoded versions + $position = 0; + $strlen = strlen($string); + while (($position += strspn($string, $extra_chars, $position)) < $strlen) + { + $value = ord($string[$position]); + + // Start position + $start = $position; + + // By default we are valid + $valid = true; + + // No one byte sequences are valid due to the while. + // Two byte sequence: + if (($value & 0xE0) === 0xC0) + { + $character = ($value & 0x1F) << 6; + $length = 2; + $remaining = 1; + } + // Three byte sequence: + elseif (($value & 0xF0) === 0xE0) + { + $character = ($value & 0x0F) << 12; + $length = 3; + $remaining = 2; + } + // Four byte sequence: + elseif (($value & 0xF8) === 0xF0) + { + $character = ($value & 0x07) << 18; + $length = 4; + $remaining = 3; + } + // Invalid byte: + else + { + $valid = false; + $length = 1; + $remaining = 0; + } + + if ($remaining) + { + if ($position + $length <= $strlen) + { + for ($position++; $remaining; $position++) + { + $value = ord($string[$position]); + + // Check that the byte is valid, then add it to the character: + if (($value & 0xC0) === 0x80) + { + $character |= ($value & 0x3F) << (--$remaining * 6); + } + // If it is invalid, count the sequence as invalid and reprocess the current byte: + else + { + $valid = false; + $position--; + break; + } + } + } + else + { + $position = $strlen - 1; + $valid = false; + } + } + + // Percent encode anything invalid or not in ucschar + if ( + // Invalid sequences + !$valid + // Non-shortest form sequences are invalid + || $length > 1 && $character <= 0x7F + || $length > 2 && $character <= 0x7FF + || $length > 3 && $character <= 0xFFFF + // Outside of range of ucschar codepoints + // Noncharacters + || ($character & 0xFFFE) === 0xFFFE + || $character >= 0xFDD0 && $character <= 0xFDEF + || ( + // Everything else not in ucschar + $character > 0xD7FF && $character < 0xF900 + || $character < 0xA0 + || $character > 0xEFFFD + ) + && ( + // Everything not in iprivate, if it applies + !$iprivate + || $character < 0xE000 + || $character > 0x10FFFD + ) + ) + { + // If we were a character, pretend we weren't, but rather an error. + if ($valid) + $position--; + + for ($j = $start; $j <= $position; $j++) + { + $string = substr_replace($string, sprintf('%%%02X', ord($string[$j])), $j, 1); + $j += 2; + $position += 2; + $strlen += 2; + } + } + } + + return $string; + } + + /** + * Callback function for preg_replace_callback. + * + * Removes sequences of percent encoded bytes that represent UTF-8 + * encoded characters in iunreserved + * + * @param array $match PCRE match + * @return string Replacement + */ + protected function remove_iunreserved_percent_encoded($match) + { + // As we just have valid percent encoded sequences we can just explode + // and ignore the first member of the returned array (an empty string). + $bytes = explode('%', $match[0]); + + // Initialize the new string (this is what will be returned) and that + // there are no bytes remaining in the current sequence (unsurprising + // at the first byte!). + $string = ''; + $remaining = 0; + + // Loop over each and every byte, and set $value to its value + for ($i = 1, $len = count($bytes); $i < $len; $i++) + { + $value = hexdec($bytes[$i]); + + // If we're the first byte of sequence: + if (!$remaining) + { + // Start position + $start = $i; + + // By default we are valid + $valid = true; + + // One byte sequence: + if ($value <= 0x7F) + { + $character = $value; + $length = 1; + } + // Two byte sequence: + elseif (($value & 0xE0) === 0xC0) + { + $character = ($value & 0x1F) << 6; + $length = 2; + $remaining = 1; + } + // Three byte sequence: + elseif (($value & 0xF0) === 0xE0) + { + $character = ($value & 0x0F) << 12; + $length = 3; + $remaining = 2; + } + // Four byte sequence: + elseif (($value & 0xF8) === 0xF0) + { + $character = ($value & 0x07) << 18; + $length = 4; + $remaining = 3; + } + // Invalid byte: + else + { + $valid = false; + $remaining = 0; + } + } + // Continuation byte: + else + { + // Check that the byte is valid, then add it to the character: + if (($value & 0xC0) === 0x80) + { + $remaining--; + $character |= ($value & 0x3F) << ($remaining * 6); + } + // If it is invalid, count the sequence as invalid and reprocess the current byte as the start of a sequence: + else + { + $valid = false; + $remaining = 0; + $i--; + } + } + + // If we've reached the end of the current byte sequence, append it to Unicode::$data + if (!$remaining) + { + // Percent encode anything invalid or not in iunreserved + if ( + // Invalid sequences + !$valid + // Non-shortest form sequences are invalid + || $length > 1 && $character <= 0x7F + || $length > 2 && $character <= 0x7FF + || $length > 3 && $character <= 0xFFFF + // Outside of range of iunreserved codepoints + || $character < 0x2D + || $character > 0xEFFFD + // Noncharacters + || ($character & 0xFFFE) === 0xFFFE + || $character >= 0xFDD0 && $character <= 0xFDEF + // Everything else not in iunreserved (this is all BMP) + || $character === 0x2F + || $character > 0x39 && $character < 0x41 + || $character > 0x5A && $character < 0x61 + || $character > 0x7A && $character < 0x7E + || $character > 0x7E && $character < 0xA0 + || $character > 0xD7FF && $character < 0xF900 + ) + { + for ($j = $start; $j <= $i; $j++) + { + $string .= '%' . strtoupper($bytes[$j]); + } + } + else + { + for ($j = $start; $j <= $i; $j++) + { + $string .= chr(hexdec($bytes[$j])); + } + } + } + } + + // If we have any bytes left over they are invalid (i.e., we are + // mid-way through a multi-byte sequence) + if ($remaining) + { + for ($j = $start; $j < $len; $j++) + { + $string .= '%' . strtoupper($bytes[$j]); + } + } + + return $string; + } + + protected function scheme_normalization() + { + if (isset($this->normalization[$this->scheme]['iuserinfo']) && $this->iuserinfo === $this->normalization[$this->scheme]['iuserinfo']) + { + $this->iuserinfo = null; + } + if (isset($this->normalization[$this->scheme]['ihost']) && $this->ihost === $this->normalization[$this->scheme]['ihost']) + { + $this->ihost = null; + } + if (isset($this->normalization[$this->scheme]['port']) && $this->port === $this->normalization[$this->scheme]['port']) + { + $this->port = null; + } + if (isset($this->normalization[$this->scheme]['ipath']) && $this->ipath === $this->normalization[$this->scheme]['ipath']) + { + $this->ipath = ''; + } + if (isset($this->normalization[$this->scheme]['iquery']) && $this->iquery === $this->normalization[$this->scheme]['iquery']) + { + $this->iquery = null; + } + if (isset($this->normalization[$this->scheme]['ifragment']) && $this->ifragment === $this->normalization[$this->scheme]['ifragment']) + { + $this->ifragment = null; + } + } + + /** + * Check if the object represents a valid IRI. This needs to be done on each + * call as some things change depending on another part of the IRI. + * + * @return bool + */ + public function is_valid() + { + if ($this->ipath === '') return true; + + $isauthority = $this->iuserinfo !== null || $this->ihost !== null || + $this->port !== null; + if ($isauthority && $this->ipath[0] === '/') return true; + + if (!$isauthority && (substr($this->ipath, 0, 2) === '//')) return false; + + // Relative urls cannot have a colon in the first path segment (and the + // slashes themselves are not included so skip the first character). + if (!$this->scheme && !$isauthority && + strpos($this->ipath, ':') !== false && + strpos($this->ipath, '/', 1) !== false && + strpos($this->ipath, ':') < strpos($this->ipath, '/', 1)) return false; + + return true; + } + + /** + * Set the entire IRI. Returns true on success, false on failure (if there + * are any invalid characters). + * + * @param string $iri + * @return bool + */ + public function set_iri($iri, $clear_cache = false) + { + static $cache; + if ($clear_cache) + { + $cache = null; + return; + } + if (!$cache) + { + $cache = array(); + } + + if ($iri === null) + { + return true; + } + elseif (isset($cache[$iri])) + { + list($this->scheme, + $this->iuserinfo, + $this->ihost, + $this->port, + $this->ipath, + $this->iquery, + $this->ifragment, + $return) = $cache[$iri]; + return $return; + } + else + { + $parsed = $this->parse_iri((string) $iri); + if (!$parsed) + { + return false; + } + + $return = $this->set_scheme($parsed['scheme']) + && $this->set_authority($parsed['authority']) + && $this->set_path($parsed['path']) + && $this->set_query($parsed['query']) + && $this->set_fragment($parsed['fragment']); + + $cache[$iri] = array($this->scheme, + $this->iuserinfo, + $this->ihost, + $this->port, + $this->ipath, + $this->iquery, + $this->ifragment, + $return); + return $return; + } + } + + /** + * Set the scheme. Returns true on success, false on failure (if there are + * any invalid characters). + * + * @param string $scheme + * @return bool + */ + public function set_scheme($scheme) + { + if ($scheme === null) + { + $this->scheme = null; + } + elseif (!preg_match('/^[A-Za-z][0-9A-Za-z+\-.]*$/', $scheme)) + { + $this->scheme = null; + return false; + } + else + { + $this->scheme = strtolower($scheme); + } + return true; + } + + /** + * Set the authority. Returns true on success, false on failure (if there are + * any invalid characters). + * + * @param string $authority + * @return bool + */ + public function set_authority($authority, $clear_cache = false) + { + static $cache; + if ($clear_cache) + { + $cache = null; + return; + } + if (!$cache) + $cache = array(); + + if ($authority === null) + { + $this->iuserinfo = null; + $this->ihost = null; + $this->port = null; + return true; + } + elseif (isset($cache[$authority])) + { + list($this->iuserinfo, + $this->ihost, + $this->port, + $return) = $cache[$authority]; + + return $return; + } + else + { + $remaining = $authority; + if (($iuserinfo_end = strrpos($remaining, '@')) !== false) + { + $iuserinfo = substr($remaining, 0, $iuserinfo_end); + $remaining = substr($remaining, $iuserinfo_end + 1); + } + else + { + $iuserinfo = null; + } + if (($port_start = strpos($remaining, ':', strpos($remaining, ']'))) !== false) + { + if (($port = substr($remaining, $port_start + 1)) === false) + { + $port = null; + } + $remaining = substr($remaining, 0, $port_start); + } + else + { + $port = null; + } + + $return = $this->set_userinfo($iuserinfo) && + $this->set_host($remaining) && + $this->set_port($port); + + $cache[$authority] = array($this->iuserinfo, + $this->ihost, + $this->port, + $return); + + return $return; + } + } + + /** + * Set the iuserinfo. + * + * @param string $iuserinfo + * @return bool + */ + public function set_userinfo($iuserinfo) + { + if ($iuserinfo === null) + { + $this->iuserinfo = null; + } + else + { + $this->iuserinfo = $this->replace_invalid_with_pct_encoding($iuserinfo, '!$&\'()*+,;=:'); + $this->scheme_normalization(); + } + + return true; + } + + /** + * Set the ihost. Returns true on success, false on failure (if there are + * any invalid characters). + * + * @param string $ihost + * @return bool + */ + public function set_host($ihost) + { + if ($ihost === null) + { + $this->ihost = null; + return true; + } + elseif (substr($ihost, 0, 1) === '[' && substr($ihost, -1) === ']') + { + if (SimplePie_Net_IPv6::check_ipv6(substr($ihost, 1, -1))) + { + $this->ihost = '[' . SimplePie_Net_IPv6::compress(substr($ihost, 1, -1)) . ']'; + } + else + { + $this->ihost = null; + return false; + } + } + else + { + $ihost = $this->replace_invalid_with_pct_encoding($ihost, '!$&\'()*+,;='); + + // Lowercase, but ignore pct-encoded sections (as they should + // remain uppercase). This must be done after the previous step + // as that can add unescaped characters. + $position = 0; + $strlen = strlen($ihost); + while (($position += strcspn($ihost, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ%', $position)) < $strlen) + { + if ($ihost[$position] === '%') + { + $position += 3; + } + else + { + $ihost[$position] = strtolower($ihost[$position]); + $position++; + } + } + + $this->ihost = $ihost; + } + + $this->scheme_normalization(); + + return true; + } + + /** + * Set the port. Returns true on success, false on failure (if there are + * any invalid characters). + * + * @param string $port + * @return bool + */ + public function set_port($port) + { + if ($port === null) + { + $this->port = null; + return true; + } + elseif (strspn($port, '0123456789') === strlen($port)) + { + $this->port = (int) $port; + $this->scheme_normalization(); + return true; + } + else + { + $this->port = null; + return false; + } + } + + /** + * Set the ipath. + * + * @param string $ipath + * @return bool + */ + public function set_path($ipath, $clear_cache = false) + { + static $cache; + if ($clear_cache) + { + $cache = null; + return; + } + if (!$cache) + { + $cache = array(); + } + + $ipath = (string) $ipath; + + if (isset($cache[$ipath])) + { + $this->ipath = $cache[$ipath][(int) ($this->scheme !== null)]; + } + else + { + $valid = $this->replace_invalid_with_pct_encoding($ipath, '!$&\'()*+,;=@:/'); + $removed = $this->remove_dot_segments($valid); + + $cache[$ipath] = array($valid, $removed); + $this->ipath = ($this->scheme !== null) ? $removed : $valid; + } + + $this->scheme_normalization(); + return true; + } + + /** + * Set the iquery. + * + * @param string $iquery + * @return bool + */ + public function set_query($iquery) + { + if ($iquery === null) + { + $this->iquery = null; + } + else + { + $this->iquery = $this->replace_invalid_with_pct_encoding($iquery, '!$&\'()*+,;=:@/?', true); + $this->scheme_normalization(); + } + return true; + } + + /** + * Set the ifragment. + * + * @param string $ifragment + * @return bool + */ + public function set_fragment($ifragment) + { + if ($ifragment === null) + { + $this->ifragment = null; + } + else + { + $this->ifragment = $this->replace_invalid_with_pct_encoding($ifragment, '!$&\'()*+,;=:@/?'); + $this->scheme_normalization(); + } + return true; + } + + /** + * Convert an IRI to a URI (or parts thereof) + * + * @return string + */ + public function to_uri($string) + { + static $non_ascii; + if (!$non_ascii) + { + $non_ascii = implode('', range("\x80", "\xFF")); + } + + $position = 0; + $strlen = strlen($string); + while (($position += strcspn($string, $non_ascii, $position)) < $strlen) + { + $string = substr_replace($string, sprintf('%%%02X', ord($string[$position])), $position, 1); + $position += 3; + $strlen += 2; + } + + return $string; + } + + /** + * Get the complete IRI + * + * @return string + */ + public function get_iri() + { + if (!$this->is_valid()) + { + return false; + } + + $iri = ''; + if ($this->scheme !== null) + { + $iri .= $this->scheme . ':'; + } + if (($iauthority = $this->get_iauthority()) !== null) + { + $iri .= '//' . $iauthority; + } + if ($this->ipath !== '') + { + $iri .= $this->ipath; + } + elseif (!empty($this->normalization[$this->scheme]['ipath']) && $iauthority !== null && $iauthority !== '') + { + $iri .= $this->normalization[$this->scheme]['ipath']; + } + if ($this->iquery !== null) + { + $iri .= '?' . $this->iquery; + } + if ($this->ifragment !== null) + { + $iri .= '#' . $this->ifragment; + } + + return $iri; + } + + /** + * Get the complete URI + * + * @return string + */ + public function get_uri() + { + return $this->to_uri($this->get_iri()); + } + + /** + * Get the complete iauthority + * + * @return string + */ + protected function get_iauthority() + { + if ($this->iuserinfo !== null || $this->ihost !== null || $this->port !== null) + { + $iauthority = ''; + if ($this->iuserinfo !== null) + { + $iauthority .= $this->iuserinfo . '@'; + } + if ($this->ihost !== null) + { + $iauthority .= $this->ihost; + } + if ($this->port !== null) + { + $iauthority .= ':' . $this->port; + } + return $iauthority; + } + else + { + return null; + } + } + + /** + * Get the complete authority + * + * @return string + */ + protected function get_authority() + { + $iauthority = $this->get_iauthority(); + if (is_string($iauthority)) + return $this->to_uri($iauthority); + else + return $iauthority; + } +} diff --git a/vendor/simplepie/simplepie/library/SimplePie/Item.php b/vendor/simplepie/simplepie/library/SimplePie/Item.php new file mode 100644 index 000000000..00f4179bf --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Item.php @@ -0,0 +1,3009 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + + +/** + * Manages all item-related data + * + * Used by {@see SimplePie::get_item()} and {@see SimplePie::get_items()} + * + * This class can be overloaded with {@see SimplePie::set_item_class()} + * + * @package SimplePie + * @subpackage API + */ +class SimplePie_Item +{ + /** + * Parent feed + * + * @access private + * @var SimplePie + */ + var $feed; + + /** + * Raw data + * + * @access private + * @var array + */ + var $data = array(); + + /** + * Registry object + * + * @see set_registry + * @var SimplePie_Registry + */ + protected $registry; + + /** + * Create a new item object + * + * This is usually used by {@see SimplePie::get_items} and + * {@see SimplePie::get_item}. Avoid creating this manually. + * + * @param SimplePie $feed Parent feed + * @param array $data Raw data + */ + public function __construct($feed, $data) + { + $this->feed = $feed; + $this->data = $data; + } + + /** + * Set the registry handler + * + * This is usually used by {@see SimplePie_Registry::create} + * + * @since 1.3 + * @param SimplePie_Registry $registry + */ + public function set_registry(SimplePie_Registry $registry) + { + $this->registry = $registry; + } + + /** + * Get a string representation of the item + * + * @return string + */ + public function __toString() + { + return md5(serialize($this->data)); + } + + /** + * Remove items that link back to this before destroying this object + */ + public function __destruct() + { + if ((version_compare(PHP_VERSION, '5.3', '<') || !gc_enabled()) && !ini_get('zend.ze1_compatibility_mode')) + { + unset($this->feed); + } + } + + /** + * Get data for an item-level element + * + * This method allows you to get access to ANY element/attribute that is a + * sub-element of the item/entry tag. + * + * See {@see SimplePie::get_feed_tags()} for a description of the return value + * + * @since 1.0 + * @see http://simplepie.org/wiki/faq/supported_xml_namespaces + * @param string $namespace The URL of the XML namespace of the elements you're trying to access + * @param string $tag Tag name + * @return array + */ + public function get_item_tags($namespace, $tag) + { + if (isset($this->data['child'][$namespace][$tag])) + { + return $this->data['child'][$namespace][$tag]; + } + else + { + return null; + } + } + + /** + * Get the base URL value from the parent feed + * + * Uses `<xml:base>` + * + * @param array $element + * @return string + */ + public function get_base($element = array()) + { + return $this->feed->get_base($element); + } + + /** + * Sanitize feed data + * + * @access private + * @see SimplePie::sanitize() + * @param string $data Data to sanitize + * @param int $type One of the SIMPLEPIE_CONSTRUCT_* constants + * @param string $base Base URL to resolve URLs against + * @return string Sanitized data + */ + public function sanitize($data, $type, $base = '') + { + return $this->feed->sanitize($data, $type, $base); + } + + /** + * Get the parent feed + * + * Note: this may not work as you think for multifeeds! + * + * @link http://simplepie.org/faq/typical_multifeed_gotchas#missing_data_from_feed + * @since 1.0 + * @return SimplePie + */ + public function get_feed() + { + return $this->feed; + } + + /** + * Get the unique identifier for the item + * + * This is usually used when writing code to check for new items in a feed. + * + * Uses `<atom:id>`, `<guid>`, `<dc:identifier>` or the `about` attribute + * for RDF. If none of these are supplied (or `$hash` is true), creates an + * MD5 hash based on the permalink, title and content. + * + * @since Beta 2 + * @param boolean $hash Should we force using a hash instead of the supplied ID? + * @param string|false $fn User-supplied function to generate an hash + * @return string|null + */ + public function get_id($hash = false, $fn = 'md5') + { + if (!$hash) + { + if ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'id')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'id')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'guid')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_11, 'identifier')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_10, 'identifier')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif (isset($this->data['attribs'][SIMPLEPIE_NAMESPACE_RDF]['about'])) + { + return $this->sanitize($this->data['attribs'][SIMPLEPIE_NAMESPACE_RDF]['about'], SIMPLEPIE_CONSTRUCT_TEXT); + } + } + if ($fn === false) + { + return null; + } + elseif (!is_callable($fn)) + { + trigger_error('User-supplied function $fn must be callable', E_USER_WARNING); + $fn = 'md5'; + } + return call_user_func($fn, + $this->get_permalink().$this->get_title().$this->get_content()); + } + + /** + * Get the title of the item + * + * Uses `<atom:title>`, `<title>` or `<dc:title>` + * + * @since Beta 2 (previously called `get_item_title` since 0.8) + * @return string|null + */ + public function get_title() + { + if (!isset($this->data['title'])) + { + if ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'title')) + { + $this->data['title'] = $this->sanitize($return[0]['data'], $this->registry->call('Misc', 'atom_10_construct_type', array($return[0]['attribs'])), $this->get_base($return[0])); + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'title')) + { + $this->data['title'] = $this->sanitize($return[0]['data'], $this->registry->call('Misc', 'atom_03_construct_type', array($return[0]['attribs'])), $this->get_base($return[0])); + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'title')) + { + $this->data['title'] = $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'title')) + { + $this->data['title'] = $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'title')) + { + $this->data['title'] = $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_11, 'title')) + { + $this->data['title'] = $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_10, 'title')) + { + $this->data['title'] = $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $this->data['title'] = null; + } + } + return $this->data['title']; + } + + /** + * Get the content for the item + * + * Prefers summaries over full content , but will return full content if a + * summary does not exist. + * + * To prefer full content instead, use {@see get_content} + * + * Uses `<atom:summary>`, `<description>`, `<dc:description>` or + * `<itunes:subtitle>` + * + * @since 0.8 + * @param boolean $description_only Should we avoid falling back to the content? + * @return string|null + */ + public function get_description($description_only = false) + { + if (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'summary')) && + ($return = $this->sanitize($tags[0]['data'], $this->registry->call('Misc', 'atom_10_construct_type', array($tags[0]['attribs'])), $this->get_base($tags[0])))) + { + return $return; + } + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'summary')) && + ($return = $this->sanitize($tags[0]['data'], $this->registry->call('Misc', 'atom_03_construct_type', array($tags[0]['attribs'])), $this->get_base($tags[0])))) + { + return $return; + } + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'description')) && + ($return = $this->sanitize($tags[0]['data'], SIMPLEPIE_CONSTRUCT_MAYBE_HTML, $this->get_base($tags[0])))) + { + return $return; + } + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'description')) && + ($return = $this->sanitize($tags[0]['data'], SIMPLEPIE_CONSTRUCT_HTML, $this->get_base($tags[0])))) + { + return $return; + } + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_11, 'description')) && + ($return = $this->sanitize($tags[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT))) + { + return $return; + } + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_10, 'description')) && + ($return = $this->sanitize($tags[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT))) + { + return $return; + } + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'summary')) && + ($return = $this->sanitize($tags[0]['data'], SIMPLEPIE_CONSTRUCT_HTML, $this->get_base($tags[0])))) + { + return $return; + } + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'subtitle')) && + ($return = $this->sanitize($tags[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT))) + { + return $return; + } + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'description')) && + ($return = $this->sanitize($tags[0]['data'], SIMPLEPIE_CONSTRUCT_HTML))) + { + return $return; + } + + elseif (!$description_only) + { + return $this->get_content(true); + } + else + { + return null; + } + } + + /** + * Get the content for the item + * + * Prefers full content over summaries, but will return a summary if full + * content does not exist. + * + * To prefer summaries instead, use {@see get_description} + * + * Uses `<atom:content>` or `<content:encoded>` (RSS 1.0 Content Module) + * + * @since 1.0 + * @param boolean $content_only Should we avoid falling back to the description? + * @return string|null + */ + public function get_content($content_only = false) + { + if (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'content')) && + ($return = $this->sanitize($tags[0]['data'], $this->registry->call('Misc', 'atom_10_content_construct_type', array($tags[0]['attribs'])), $this->get_base($tags[0])))) + { + return $return; + } + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'content')) && + ($return = $this->sanitize($tags[0]['data'], $this->registry->call('Misc', 'atom_03_construct_type', array($tags[0]['attribs'])), $this->get_base($tags[0])))) + { + return $return; + } + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_10_MODULES_CONTENT, 'encoded')) && + ($return = $this->sanitize($tags[0]['data'], SIMPLEPIE_CONSTRUCT_HTML, $this->get_base($tags[0])))) + { + return $return; + } + elseif (!$content_only) + { + return $this->get_description(true); + } + else + { + return null; + } + } + + /** + * Get the media:thumbnail of the item + * + * Uses `<media:thumbnail>` + * + * + * @return array|null + */ + public function get_thumbnail() + { + if (!isset($this->data['thumbnail'])) + { + if ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'thumbnail')) + { + $this->data['thumbnail'] = $return[0]['attribs']['']; + } + else + { + $this->data['thumbnail'] = null; + } + } + return $this->data['thumbnail']; + } + + /** + * Get a category for the item + * + * @since Beta 3 (previously called `get_categories()` since Beta 2) + * @param int $key The category that you want to return. Remember that arrays begin with 0, not 1 + * @return SimplePie_Category|null + */ + public function get_category($key = 0) + { + $categories = $this->get_categories(); + if (isset($categories[$key])) + { + return $categories[$key]; + } + else + { + return null; + } + } + + /** + * Get all categories for the item + * + * Uses `<atom:category>`, `<category>` or `<dc:subject>` + * + * @since Beta 3 + * @return SimplePie_Category[]|null List of {@see SimplePie_Category} objects + */ + public function get_categories() + { + $categories = array(); + + $type = 'category'; + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, $type) as $category) + { + $term = null; + $scheme = null; + $label = null; + if (isset($category['attribs']['']['term'])) + { + $term = $this->sanitize($category['attribs']['']['term'], SIMPLEPIE_CONSTRUCT_HTML); + } + if (isset($category['attribs']['']['scheme'])) + { + $scheme = $this->sanitize($category['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_HTML); + } + if (isset($category['attribs']['']['label'])) + { + $label = $this->sanitize($category['attribs']['']['label'], SIMPLEPIE_CONSTRUCT_HTML); + } + $categories[] = $this->registry->create('Category', array($term, $scheme, $label, $type)); + } + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, $type) as $category) + { + // This is really the label, but keep this as the term also for BC. + // Label will also work on retrieving because that falls back to term. + $term = $this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_HTML); + if (isset($category['attribs']['']['domain'])) + { + $scheme = $this->sanitize($category['attribs']['']['domain'], SIMPLEPIE_CONSTRUCT_HTML); + } + else + { + $scheme = null; + } + $categories[] = $this->registry->create('Category', array($term, $scheme, null, $type)); + } + + $type = 'subject'; + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_11, $type) as $category) + { + $categories[] = $this->registry->create('Category', array($this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_HTML), null, null, $type)); + } + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_10, $type) as $category) + { + $categories[] = $this->registry->create('Category', array($this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_HTML), null, null, $type)); + } + + if (!empty($categories)) + { + return array_unique($categories); + } + else + { + return null; + } + } + + /** + * Get an author for the item + * + * @since Beta 2 + * @param int $key The author that you want to return. Remember that arrays begin with 0, not 1 + * @return SimplePie_Author|null + */ + public function get_author($key = 0) + { + $authors = $this->get_authors(); + if (isset($authors[$key])) + { + return $authors[$key]; + } + else + { + return null; + } + } + + /** + * Get a contributor for the item + * + * @since 1.1 + * @param int $key The contrbutor that you want to return. Remember that arrays begin with 0, not 1 + * @return SimplePie_Author|null + */ + public function get_contributor($key = 0) + { + $contributors = $this->get_contributors(); + if (isset($contributors[$key])) + { + return $contributors[$key]; + } + else + { + return null; + } + } + + /** + * Get all contributors for the item + * + * Uses `<atom:contributor>` + * + * @since 1.1 + * @return array|null List of {@see SimplePie_Author} objects + */ + public function get_contributors() + { + $contributors = array(); + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'contributor') as $contributor) + { + $name = null; + $uri = null; + $email = null; + if (isset($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data'])) + { + $name = $this->sanitize($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data'])) + { + $uri = $this->sanitize($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0])); + } + if (isset($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['email'][0]['data'])) + { + $email = $this->sanitize($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['email'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $uri !== null) + { + $contributors[] = $this->registry->create('Author', array($name, $uri, $email)); + } + } + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'contributor') as $contributor) + { + $name = null; + $url = null; + $email = null; + if (isset($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['name'][0]['data'])) + { + $name = $this->sanitize($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['name'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0]['data'])) + { + $url = $this->sanitize($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0])); + } + if (isset($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['email'][0]['data'])) + { + $email = $this->sanitize($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['email'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $url !== null) + { + $contributors[] = $this->registry->create('Author', array($name, $url, $email)); + } + } + + if (!empty($contributors)) + { + return array_unique($contributors); + } + else + { + return null; + } + } + + /** + * Get all authors for the item + * + * Uses `<atom:author>`, `<author>`, `<dc:creator>` or `<itunes:author>` + * + * @since Beta 2 + * @return array|null List of {@see SimplePie_Author} objects + */ + public function get_authors() + { + $authors = array(); + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'author') as $author) + { + $name = null; + $uri = null; + $email = null; + if (isset($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data'])) + { + $name = $this->sanitize($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data'], SIMPLEPIE_CONSTRUCT_HTML); + } + if (isset($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data'])) + { + $uri = $this->sanitize($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0])); + } + if (isset($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['email'][0]['data'])) + { + $email = $this->sanitize($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['email'][0]['data'], SIMPLEPIE_CONSTRUCT_HTML); + } + if ($name !== null || $email !== null || $uri !== null) + { + $authors[] = $this->registry->create('Author', array($name, $uri, $email)); + } + } + if ($author = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'author')) + { + $name = null; + $url = null; + $email = null; + if (isset($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['name'][0]['data'])) + { + $name = $this->sanitize($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['name'][0]['data'], SIMPLEPIE_CONSTRUCT_HTML); + } + if (isset($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0]['data'])) + { + $url = $this->sanitize($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0])); + } + if (isset($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['email'][0]['data'])) + { + $email = $this->sanitize($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['email'][0]['data'], SIMPLEPIE_CONSTRUCT_HTML); + } + if ($name !== null || $email !== null || $url !== null) + { + $authors[] = $this->registry->create('Author', array($name, $url, $email)); + } + } + if ($author = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'author')) + { + $authors[] = $this->registry->create('Author', array(null, null, $this->sanitize($author[0]['data'], SIMPLEPIE_CONSTRUCT_HTML))); + } + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_11, 'creator') as $author) + { + $authors[] = $this->registry->create('Author', array($this->sanitize($author['data'], SIMPLEPIE_CONSTRUCT_HTML), null, null)); + } + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_10, 'creator') as $author) + { + $authors[] = $this->registry->create('Author', array($this->sanitize($author['data'], SIMPLEPIE_CONSTRUCT_HTML), null, null)); + } + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'author') as $author) + { + $authors[] = $this->registry->create('Author', array($this->sanitize($author['data'], SIMPLEPIE_CONSTRUCT_HTML), null, null)); + } + + if (!empty($authors)) + { + return array_unique($authors); + } + elseif (($source = $this->get_source()) && ($authors = $source->get_authors())) + { + return $authors; + } + elseif ($authors = $this->feed->get_authors()) + { + return $authors; + } + else + { + return null; + } + } + + /** + * Get the copyright info for the item + * + * Uses `<atom:rights>` or `<dc:rights>` + * + * @since 1.1 + * @return string + */ + public function get_copyright() + { + if ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'rights')) + { + return $this->sanitize($return[0]['data'], $this->registry->call('Misc', 'atom_10_construct_type', array($return[0]['attribs'])), $this->get_base($return[0])); + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_11, 'rights')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_10, 'rights')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + return null; + } + } + + /** + * Get the posting date/time for the item + * + * Uses `<atom:published>`, `<atom:updated>`, `<atom:issued>`, + * `<atom:modified>`, `<pubDate>` or `<dc:date>` + * + * Note: obeys PHP's timezone setting. To get a UTC date/time, use + * {@see get_gmdate} + * + * @since Beta 2 (previously called `get_item_date` since 0.8) + * + * @param string $date_format Supports any PHP date format from {@see http://php.net/date} (empty for the raw data) + * @return int|string|null + */ + public function get_date($date_format = 'j F Y, g:i a') + { + if (!isset($this->data['date'])) + { + if ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'published')) + { + $this->data['date']['raw'] = $return[0]['data']; + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'pubDate')) + { + $this->data['date']['raw'] = $return[0]['data']; + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_11, 'date')) + { + $this->data['date']['raw'] = $return[0]['data']; + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_10, 'date')) + { + $this->data['date']['raw'] = $return[0]['data']; + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'updated')) + { + $this->data['date']['raw'] = $return[0]['data']; + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'issued')) + { + $this->data['date']['raw'] = $return[0]['data']; + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'created')) + { + $this->data['date']['raw'] = $return[0]['data']; + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'modified')) + { + $this->data['date']['raw'] = $return[0]['data']; + } + + if (!empty($this->data['date']['raw'])) + { + $parser = $this->registry->call('Parse_Date', 'get'); + $this->data['date']['parsed'] = $parser->parse($this->data['date']['raw']); + } + else + { + $this->data['date'] = null; + } + } + if ($this->data['date']) + { + $date_format = (string) $date_format; + switch ($date_format) + { + case '': + return $this->sanitize($this->data['date']['raw'], SIMPLEPIE_CONSTRUCT_TEXT); + + case 'U': + return $this->data['date']['parsed']; + + default: + return date($date_format, $this->data['date']['parsed']); + } + } + else + { + return null; + } + } + + /** + * Get the update date/time for the item + * + * Uses `<atom:updated>` + * + * Note: obeys PHP's timezone setting. To get a UTC date/time, use + * {@see get_gmdate} + * + * @param string $date_format Supports any PHP date format from {@see http://php.net/date} (empty for the raw data) + * @return int|string|null + */ + public function get_updated_date($date_format = 'j F Y, g:i a') + { + if (!isset($this->data['updated'])) + { + if ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'updated')) + { + $this->data['updated']['raw'] = $return[0]['data']; + } + + if (!empty($this->data['updated']['raw'])) + { + $parser = $this->registry->call('Parse_Date', 'get'); + $this->data['updated']['parsed'] = $parser->parse($this->data['updated']['raw']); + } + else + { + $this->data['updated'] = null; + } + } + if ($this->data['updated']) + { + $date_format = (string) $date_format; + switch ($date_format) + { + case '': + return $this->sanitize($this->data['updated']['raw'], SIMPLEPIE_CONSTRUCT_TEXT); + + case 'U': + return $this->data['updated']['parsed']; + + default: + return date($date_format, $this->data['updated']['parsed']); + } + } + else + { + return null; + } + } + + /** + * Get the localized posting date/time for the item + * + * Returns the date formatted in the localized language. To display in + * languages other than the server's default, you need to change the locale + * with {@link http://php.net/setlocale setlocale()}. The available + * localizations depend on which ones are installed on your web server. + * + * @since 1.0 + * + * @param string $date_format Supports any PHP date format from {@see http://php.net/strftime} (empty for the raw data) + * @return int|string|null + */ + public function get_local_date($date_format = '%c') + { + if (!$date_format) + { + return $this->sanitize($this->get_date(''), SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif (($date = $this->get_date('U')) !== null && $date !== false) + { + return strftime($date_format, $date); + } + else + { + return null; + } + } + + /** + * Get the posting date/time for the item (UTC time) + * + * @see get_date + * @param string $date_format Supports any PHP date format from {@see http://php.net/date} + * @return int|string|null + */ + public function get_gmdate($date_format = 'j F Y, g:i a') + { + $date = $this->get_date('U'); + if ($date === null) + { + return null; + } + + return gmdate($date_format, $date); + } + + /** + * Get the update date/time for the item (UTC time) + * + * @see get_updated_date + * @param string $date_format Supports any PHP date format from {@see http://php.net/date} + * @return int|string|null + */ + public function get_updated_gmdate($date_format = 'j F Y, g:i a') + { + $date = $this->get_updated_date('U'); + if ($date === null) + { + return null; + } + + return gmdate($date_format, $date); + } + + /** + * Get the permalink for the item + * + * Returns the first link available with a relationship of "alternate". + * Identical to {@see get_link()} with key 0 + * + * @see get_link + * @since 0.8 + * @return string|null Permalink URL + */ + public function get_permalink() + { + $link = $this->get_link(); + $enclosure = $this->get_enclosure(0); + if ($link !== null) + { + return $link; + } + elseif ($enclosure !== null) + { + return $enclosure->get_link(); + } + else + { + return null; + } + } + + /** + * Get a single link for the item + * + * @since Beta 3 + * @param int $key The link that you want to return. Remember that arrays begin with 0, not 1 + * @param string $rel The relationship of the link to return + * @return string|null Link URL + */ + public function get_link($key = 0, $rel = 'alternate') + { + $links = $this->get_links($rel); + if ($links[$key] !== null) + { + return $links[$key]; + } + else + { + return null; + } + } + + /** + * Get all links for the item + * + * Uses `<atom:link>`, `<link>` or `<guid>` + * + * @since Beta 2 + * @param string $rel The relationship of links to return + * @return array|null Links found for the item (strings) + */ + public function get_links($rel = 'alternate') + { + if (!isset($this->data['links'])) + { + $this->data['links'] = array(); + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'link') as $link) + { + if (isset($link['attribs']['']['href'])) + { + $link_rel = (isset($link['attribs']['']['rel'])) ? $link['attribs']['']['rel'] : 'alternate'; + $this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($link)); + + } + } + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'link') as $link) + { + if (isset($link['attribs']['']['href'])) + { + $link_rel = (isset($link['attribs']['']['rel'])) ? $link['attribs']['']['rel'] : 'alternate'; + $this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($link)); + } + } + if ($links = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'link')) + { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($links[0])); + } + if ($links = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'link')) + { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($links[0])); + } + if ($links = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'link')) + { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($links[0])); + } + if ($links = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'guid')) + { + if (!isset($links[0]['attribs']['']['isPermaLink']) || strtolower(trim($links[0]['attribs']['']['isPermaLink'])) === 'true') + { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($links[0])); + } + } + + $keys = array_keys($this->data['links']); + foreach ($keys as $key) + { + if ($this->registry->call('Misc', 'is_isegment_nz_nc', array($key))) + { + if (isset($this->data['links'][SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY . $key])) + { + $this->data['links'][SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY . $key] = array_merge($this->data['links'][$key], $this->data['links'][SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY . $key]); + $this->data['links'][$key] =& $this->data['links'][SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY . $key]; + } + else + { + $this->data['links'][SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY . $key] =& $this->data['links'][$key]; + } + } + elseif (substr($key, 0, 41) === SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY) + { + $this->data['links'][substr($key, 41)] =& $this->data['links'][$key]; + } + $this->data['links'][$key] = array_unique($this->data['links'][$key]); + } + } + if (isset($this->data['links'][$rel])) + { + return $this->data['links'][$rel]; + } + else + { + return null; + } + } + + /** + * Get an enclosure from the item + * + * Supports the <enclosure> RSS tag, as well as Media RSS and iTunes RSS. + * + * @since Beta 2 + * @todo Add ability to prefer one type of content over another (in a media group). + * @param int $key The enclosure that you want to return. Remember that arrays begin with 0, not 1 + * @return SimplePie_Enclosure|null + */ + public function get_enclosure($key = 0, $prefer = null) + { + $enclosures = $this->get_enclosures(); + if (isset($enclosures[$key])) + { + return $enclosures[$key]; + } + else + { + return null; + } + } + + /** + * Get all available enclosures (podcasts, etc.) + * + * Supports the <enclosure> RSS tag, as well as Media RSS and iTunes RSS. + * + * At this point, we're pretty much assuming that all enclosures for an item + * are the same content. Anything else is too complicated to + * properly support. + * + * @since Beta 2 + * @todo Add support for end-user defined sorting of enclosures by type/handler (so we can prefer the faster-loading FLV over MP4). + * @todo If an element exists at a level, but its value is empty, we should fall back to the value from the parent (if it exists). + * @return SimplePie_Enclosure[]|null List of SimplePie_Enclosure items + */ + public function get_enclosures() + { + if (!isset($this->data['enclosures'])) + { + $this->data['enclosures'] = array(); + + // Elements + $captions_parent = null; + $categories_parent = null; + $copyrights_parent = null; + $credits_parent = null; + $description_parent = null; + $duration_parent = null; + $hashes_parent = null; + $keywords_parent = null; + $player_parent = null; + $ratings_parent = null; + $restrictions_parent = null; + $thumbnails_parent = null; + $title_parent = null; + + // Let's do the channel and item-level ones first, and just re-use them if we need to. + $parent = $this->get_feed(); + + // CAPTIONS + if ($captions = $this->get_item_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'text')) + { + foreach ($captions as $caption) + { + $caption_type = null; + $caption_lang = null; + $caption_startTime = null; + $caption_endTime = null; + $caption_text = null; + if (isset($caption['attribs']['']['type'])) + { + $caption_type = $this->sanitize($caption['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['lang'])) + { + $caption_lang = $this->sanitize($caption['attribs']['']['lang'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['start'])) + { + $caption_startTime = $this->sanitize($caption['attribs']['']['start'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['end'])) + { + $caption_endTime = $this->sanitize($caption['attribs']['']['end'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['data'])) + { + $caption_text = $this->sanitize($caption['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $captions_parent[] = $this->registry->create('Caption', array($caption_type, $caption_lang, $caption_startTime, $caption_endTime, $caption_text)); + } + } + elseif ($captions = $parent->get_channel_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'text')) + { + foreach ($captions as $caption) + { + $caption_type = null; + $caption_lang = null; + $caption_startTime = null; + $caption_endTime = null; + $caption_text = null; + if (isset($caption['attribs']['']['type'])) + { + $caption_type = $this->sanitize($caption['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['lang'])) + { + $caption_lang = $this->sanitize($caption['attribs']['']['lang'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['start'])) + { + $caption_startTime = $this->sanitize($caption['attribs']['']['start'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['end'])) + { + $caption_endTime = $this->sanitize($caption['attribs']['']['end'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['data'])) + { + $caption_text = $this->sanitize($caption['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $captions_parent[] = $this->registry->create('Caption', array($caption_type, $caption_lang, $caption_startTime, $caption_endTime, $caption_text)); + } + } + if (is_array($captions_parent)) + { + $captions_parent = array_values(array_unique($captions_parent)); + } + + // CATEGORIES + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'category') as $category) + { + $term = null; + $scheme = null; + $label = null; + if (isset($category['data'])) + { + $term = $this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['scheme'])) + { + $scheme = $this->sanitize($category['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $scheme = 'http://search.yahoo.com/mrss/category_schema'; + } + if (isset($category['attribs']['']['label'])) + { + $label = $this->sanitize($category['attribs']['']['label'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $categories_parent[] = $this->registry->create('Category', array($term, $scheme, $label)); + } + foreach ((array) $parent->get_channel_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'category') as $category) + { + $term = null; + $scheme = null; + $label = null; + if (isset($category['data'])) + { + $term = $this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['scheme'])) + { + $scheme = $this->sanitize($category['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $scheme = 'http://search.yahoo.com/mrss/category_schema'; + } + if (isset($category['attribs']['']['label'])) + { + $label = $this->sanitize($category['attribs']['']['label'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $categories_parent[] = $this->registry->create('Category', array($term, $scheme, $label)); + } + foreach ((array) $parent->get_channel_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'category') as $category) + { + $term = null; + $scheme = 'http://www.itunes.com/dtds/podcast-1.0.dtd'; + $label = null; + if (isset($category['attribs']['']['text'])) + { + $label = $this->sanitize($category['attribs']['']['text'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $categories_parent[] = $this->registry->create('Category', array($term, $scheme, $label)); + + if (isset($category['child'][SIMPLEPIE_NAMESPACE_ITUNES]['category'])) + { + foreach ((array) $category['child'][SIMPLEPIE_NAMESPACE_ITUNES]['category'] as $subcategory) + { + if (isset($subcategory['attribs']['']['text'])) + { + $label = $this->sanitize($subcategory['attribs']['']['text'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $categories_parent[] = $this->registry->create('Category', array($term, $scheme, $label)); + } + } + } + if (is_array($categories_parent)) + { + $categories_parent = array_values(array_unique($categories_parent)); + } + + // COPYRIGHT + if ($copyright = $this->get_item_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'copyright')) + { + $copyright_url = null; + $copyright_label = null; + if (isset($copyright[0]['attribs']['']['url'])) + { + $copyright_url = $this->sanitize($copyright[0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($copyright[0]['data'])) + { + $copyright_label = $this->sanitize($copyright[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $copyrights_parent = $this->registry->create('Copyright', array($copyright_url, $copyright_label)); + } + elseif ($copyright = $parent->get_channel_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'copyright')) + { + $copyright_url = null; + $copyright_label = null; + if (isset($copyright[0]['attribs']['']['url'])) + { + $copyright_url = $this->sanitize($copyright[0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($copyright[0]['data'])) + { + $copyright_label = $this->sanitize($copyright[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $copyrights_parent = $this->registry->create('Copyright', array($copyright_url, $copyright_label)); + } + + // CREDITS + if ($credits = $this->get_item_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'credit')) + { + foreach ($credits as $credit) + { + $credit_role = null; + $credit_scheme = null; + $credit_name = null; + if (isset($credit['attribs']['']['role'])) + { + $credit_role = $this->sanitize($credit['attribs']['']['role'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($credit['attribs']['']['scheme'])) + { + $credit_scheme = $this->sanitize($credit['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $credit_scheme = 'urn:ebu'; + } + if (isset($credit['data'])) + { + $credit_name = $this->sanitize($credit['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $credits_parent[] = $this->registry->create('Credit', array($credit_role, $credit_scheme, $credit_name)); + } + } + elseif ($credits = $parent->get_channel_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'credit')) + { + foreach ($credits as $credit) + { + $credit_role = null; + $credit_scheme = null; + $credit_name = null; + if (isset($credit['attribs']['']['role'])) + { + $credit_role = $this->sanitize($credit['attribs']['']['role'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($credit['attribs']['']['scheme'])) + { + $credit_scheme = $this->sanitize($credit['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $credit_scheme = 'urn:ebu'; + } + if (isset($credit['data'])) + { + $credit_name = $this->sanitize($credit['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $credits_parent[] = $this->registry->create('Credit', array($credit_role, $credit_scheme, $credit_name)); + } + } + if (is_array($credits_parent)) + { + $credits_parent = array_values(array_unique($credits_parent)); + } + + // DESCRIPTION + if ($description_parent = $this->get_item_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'description')) + { + if (isset($description_parent[0]['data'])) + { + $description_parent = $this->sanitize($description_parent[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + } + elseif ($description_parent = $parent->get_channel_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'description')) + { + if (isset($description_parent[0]['data'])) + { + $description_parent = $this->sanitize($description_parent[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + } + + // DURATION + if ($duration_parent = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'duration')) + { + $seconds = null; + $minutes = null; + $hours = null; + if (isset($duration_parent[0]['data'])) + { + $temp = explode(':', $this->sanitize($duration_parent[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT)); + if (sizeof($temp) > 0) + { + $seconds = (int) array_pop($temp); + } + if (sizeof($temp) > 0) + { + $minutes = (int) array_pop($temp); + $seconds += $minutes * 60; + } + if (sizeof($temp) > 0) + { + $hours = (int) array_pop($temp); + $seconds += $hours * 3600; + } + unset($temp); + $duration_parent = $seconds; + } + } + + // HASHES + if ($hashes_iterator = $this->get_item_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'hash')) + { + foreach ($hashes_iterator as $hash) + { + $value = null; + $algo = null; + if (isset($hash['data'])) + { + $value = $this->sanitize($hash['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($hash['attribs']['']['algo'])) + { + $algo = $this->sanitize($hash['attribs']['']['algo'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $algo = 'md5'; + } + $hashes_parent[] = $algo.':'.$value; + } + } + elseif ($hashes_iterator = $parent->get_channel_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'hash')) + { + foreach ($hashes_iterator as $hash) + { + $value = null; + $algo = null; + if (isset($hash['data'])) + { + $value = $this->sanitize($hash['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($hash['attribs']['']['algo'])) + { + $algo = $this->sanitize($hash['attribs']['']['algo'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $algo = 'md5'; + } + $hashes_parent[] = $algo.':'.$value; + } + } + if (is_array($hashes_parent)) + { + $hashes_parent = array_values(array_unique($hashes_parent)); + } + + // KEYWORDS + if ($keywords = $this->get_item_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'keywords')) + { + if (isset($keywords[0]['data'])) + { + $temp = explode(',', $this->sanitize($keywords[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT)); + foreach ($temp as $word) + { + $keywords_parent[] = trim($word); + } + } + unset($temp); + } + elseif ($keywords = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'keywords')) + { + if (isset($keywords[0]['data'])) + { + $temp = explode(',', $this->sanitize($keywords[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT)); + foreach ($temp as $word) + { + $keywords_parent[] = trim($word); + } + } + unset($temp); + } + elseif ($keywords = $parent->get_channel_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'keywords')) + { + if (isset($keywords[0]['data'])) + { + $temp = explode(',', $this->sanitize($keywords[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT)); + foreach ($temp as $word) + { + $keywords_parent[] = trim($word); + } + } + unset($temp); + } + elseif ($keywords = $parent->get_channel_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'keywords')) + { + if (isset($keywords[0]['data'])) + { + $temp = explode(',', $this->sanitize($keywords[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT)); + foreach ($temp as $word) + { + $keywords_parent[] = trim($word); + } + } + unset($temp); + } + if (is_array($keywords_parent)) + { + $keywords_parent = array_values(array_unique($keywords_parent)); + } + + // PLAYER + if ($player_parent = $this->get_item_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'player')) + { + if (isset($player_parent[0]['attribs']['']['url'])) + { + $player_parent = $this->sanitize($player_parent[0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI); + } + } + elseif ($player_parent = $parent->get_channel_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'player')) + { + if (isset($player_parent[0]['attribs']['']['url'])) + { + $player_parent = $this->sanitize($player_parent[0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI); + } + } + + // RATINGS + if ($ratings = $this->get_item_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'rating')) + { + foreach ($ratings as $rating) + { + $rating_scheme = null; + $rating_value = null; + if (isset($rating['attribs']['']['scheme'])) + { + $rating_scheme = $this->sanitize($rating['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $rating_scheme = 'urn:simple'; + } + if (isset($rating['data'])) + { + $rating_value = $this->sanitize($rating['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $ratings_parent[] = $this->registry->create('Rating', array($rating_scheme, $rating_value)); + } + } + elseif ($ratings = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'explicit')) + { + foreach ($ratings as $rating) + { + $rating_scheme = 'urn:itunes'; + $rating_value = null; + if (isset($rating['data'])) + { + $rating_value = $this->sanitize($rating['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $ratings_parent[] = $this->registry->create('Rating', array($rating_scheme, $rating_value)); + } + } + elseif ($ratings = $parent->get_channel_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'rating')) + { + foreach ($ratings as $rating) + { + $rating_scheme = null; + $rating_value = null; + if (isset($rating['attribs']['']['scheme'])) + { + $rating_scheme = $this->sanitize($rating['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $rating_scheme = 'urn:simple'; + } + if (isset($rating['data'])) + { + $rating_value = $this->sanitize($rating['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $ratings_parent[] = $this->registry->create('Rating', array($rating_scheme, $rating_value)); + } + } + elseif ($ratings = $parent->get_channel_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'explicit')) + { + foreach ($ratings as $rating) + { + $rating_scheme = 'urn:itunes'; + $rating_value = null; + if (isset($rating['data'])) + { + $rating_value = $this->sanitize($rating['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $ratings_parent[] = $this->registry->create('Rating', array($rating_scheme, $rating_value)); + } + } + if (is_array($ratings_parent)) + { + $ratings_parent = array_values(array_unique($ratings_parent)); + } + + // RESTRICTIONS + if ($restrictions = $this->get_item_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'restriction')) + { + foreach ($restrictions as $restriction) + { + $restriction_relationship = null; + $restriction_type = null; + $restriction_value = null; + if (isset($restriction['attribs']['']['relationship'])) + { + $restriction_relationship = $this->sanitize($restriction['attribs']['']['relationship'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($restriction['attribs']['']['type'])) + { + $restriction_type = $this->sanitize($restriction['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($restriction['data'])) + { + $restriction_value = $this->sanitize($restriction['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $restrictions_parent[] = $this->registry->create('Restriction', array($restriction_relationship, $restriction_type, $restriction_value)); + } + } + elseif ($restrictions = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'block')) + { + foreach ($restrictions as $restriction) + { + $restriction_relationship = 'allow'; + $restriction_type = null; + $restriction_value = 'itunes'; + if (isset($restriction['data']) && strtolower($restriction['data']) === 'yes') + { + $restriction_relationship = 'deny'; + } + $restrictions_parent[] = $this->registry->create('Restriction', array($restriction_relationship, $restriction_type, $restriction_value)); + } + } + elseif ($restrictions = $parent->get_channel_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'restriction')) + { + foreach ($restrictions as $restriction) + { + $restriction_relationship = null; + $restriction_type = null; + $restriction_value = null; + if (isset($restriction['attribs']['']['relationship'])) + { + $restriction_relationship = $this->sanitize($restriction['attribs']['']['relationship'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($restriction['attribs']['']['type'])) + { + $restriction_type = $this->sanitize($restriction['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($restriction['data'])) + { + $restriction_value = $this->sanitize($restriction['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $restrictions_parent[] = $this->registry->create('Restriction', array($restriction_relationship, $restriction_type, $restriction_value)); + } + } + elseif ($restrictions = $parent->get_channel_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'block')) + { + foreach ($restrictions as $restriction) + { + $restriction_relationship = 'allow'; + $restriction_type = null; + $restriction_value = 'itunes'; + if (isset($restriction['data']) && strtolower($restriction['data']) === 'yes') + { + $restriction_relationship = 'deny'; + } + $restrictions_parent[] = $this->registry->create('Restriction', array($restriction_relationship, $restriction_type, $restriction_value)); + } + } + if (is_array($restrictions_parent)) + { + $restrictions_parent = array_values(array_unique($restrictions_parent)); + } + else + { + $restrictions_parent = array(new SimplePie_Restriction('allow', null, 'default')); + } + + // THUMBNAILS + if ($thumbnails = $this->get_item_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'thumbnail')) + { + foreach ($thumbnails as $thumbnail) + { + if (isset($thumbnail['attribs']['']['url'])) + { + $thumbnails_parent[] = $this->sanitize($thumbnail['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI); + } + } + } + elseif ($thumbnails = $parent->get_channel_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'thumbnail')) + { + foreach ($thumbnails as $thumbnail) + { + if (isset($thumbnail['attribs']['']['url'])) + { + $thumbnails_parent[] = $this->sanitize($thumbnail['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI); + } + } + } + + // TITLES + if ($title_parent = $this->get_item_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'title')) + { + if (isset($title_parent[0]['data'])) + { + $title_parent = $this->sanitize($title_parent[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + } + elseif ($title_parent = $parent->get_channel_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'title')) + { + if (isset($title_parent[0]['data'])) + { + $title_parent = $this->sanitize($title_parent[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + } + + // Clear the memory + unset($parent); + + // Attributes + $bitrate = null; + $channels = null; + $duration = null; + $expression = null; + $framerate = null; + $height = null; + $javascript = null; + $lang = null; + $length = null; + $medium = null; + $samplingrate = null; + $type = null; + $url = null; + $width = null; + + // Elements + $captions = null; + $categories = null; + $copyrights = null; + $credits = null; + $description = null; + $hashes = null; + $keywords = null; + $player = null; + $ratings = null; + $restrictions = null; + $thumbnails = null; + $title = null; + + // If we have media:group tags, loop through them. + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'group') as $group) + { + if(isset($group['child']) && isset($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['content'])) + { + // If we have media:content tags, loop through them. + foreach ((array) $group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['content'] as $content) + { + if (isset($content['attribs']['']['url'])) + { + // Attributes + $bitrate = null; + $channels = null; + $duration = null; + $expression = null; + $framerate = null; + $height = null; + $javascript = null; + $lang = null; + $length = null; + $medium = null; + $samplingrate = null; + $type = null; + $url = null; + $width = null; + + // Elements + $captions = null; + $categories = null; + $copyrights = null; + $credits = null; + $description = null; + $hashes = null; + $keywords = null; + $player = null; + $ratings = null; + $restrictions = null; + $thumbnails = null; + $title = null; + + // Start checking the attributes of media:content + if (isset($content['attribs']['']['bitrate'])) + { + $bitrate = $this->sanitize($content['attribs']['']['bitrate'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['channels'])) + { + $channels = $this->sanitize($content['attribs']['']['channels'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['duration'])) + { + $duration = $this->sanitize($content['attribs']['']['duration'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $duration = $duration_parent; + } + if (isset($content['attribs']['']['expression'])) + { + $expression = $this->sanitize($content['attribs']['']['expression'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['framerate'])) + { + $framerate = $this->sanitize($content['attribs']['']['framerate'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['height'])) + { + $height = $this->sanitize($content['attribs']['']['height'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['lang'])) + { + $lang = $this->sanitize($content['attribs']['']['lang'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['fileSize'])) + { + $length = ceil($content['attribs']['']['fileSize']); + } + if (isset($content['attribs']['']['medium'])) + { + $medium = $this->sanitize($content['attribs']['']['medium'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['samplingrate'])) + { + $samplingrate = $this->sanitize($content['attribs']['']['samplingrate'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['type'])) + { + $type = $this->sanitize($content['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['width'])) + { + $width = $this->sanitize($content['attribs']['']['width'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $url = $this->sanitize($content['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI); + + // Checking the other optional media: elements. Priority: media:content, media:group, item, channel + + // CAPTIONS + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['text'])) + { + foreach ($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['text'] as $caption) + { + $caption_type = null; + $caption_lang = null; + $caption_startTime = null; + $caption_endTime = null; + $caption_text = null; + if (isset($caption['attribs']['']['type'])) + { + $caption_type = $this->sanitize($caption['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['lang'])) + { + $caption_lang = $this->sanitize($caption['attribs']['']['lang'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['start'])) + { + $caption_startTime = $this->sanitize($caption['attribs']['']['start'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['end'])) + { + $caption_endTime = $this->sanitize($caption['attribs']['']['end'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['data'])) + { + $caption_text = $this->sanitize($caption['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $captions[] = $this->registry->create('Caption', array($caption_type, $caption_lang, $caption_startTime, $caption_endTime, $caption_text)); + } + if (is_array($captions)) + { + $captions = array_values(array_unique($captions)); + } + } + elseif (isset($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['text'])) + { + foreach ($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['text'] as $caption) + { + $caption_type = null; + $caption_lang = null; + $caption_startTime = null; + $caption_endTime = null; + $caption_text = null; + if (isset($caption['attribs']['']['type'])) + { + $caption_type = $this->sanitize($caption['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['lang'])) + { + $caption_lang = $this->sanitize($caption['attribs']['']['lang'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['start'])) + { + $caption_startTime = $this->sanitize($caption['attribs']['']['start'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['end'])) + { + $caption_endTime = $this->sanitize($caption['attribs']['']['end'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['data'])) + { + $caption_text = $this->sanitize($caption['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $captions[] = $this->registry->create('Caption', array($caption_type, $caption_lang, $caption_startTime, $caption_endTime, $caption_text)); + } + if (is_array($captions)) + { + $captions = array_values(array_unique($captions)); + } + } + else + { + $captions = $captions_parent; + } + + // CATEGORIES + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['category'])) + { + foreach ((array) $content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['category'] as $category) + { + $term = null; + $scheme = null; + $label = null; + if (isset($category['data'])) + { + $term = $this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['scheme'])) + { + $scheme = $this->sanitize($category['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $scheme = 'http://search.yahoo.com/mrss/category_schema'; + } + if (isset($category['attribs']['']['label'])) + { + $label = $this->sanitize($category['attribs']['']['label'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $categories[] = $this->registry->create('Category', array($term, $scheme, $label)); + } + } + if (isset($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['category'])) + { + foreach ((array) $group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['category'] as $category) + { + $term = null; + $scheme = null; + $label = null; + if (isset($category['data'])) + { + $term = $this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['scheme'])) + { + $scheme = $this->sanitize($category['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $scheme = 'http://search.yahoo.com/mrss/category_schema'; + } + if (isset($category['attribs']['']['label'])) + { + $label = $this->sanitize($category['attribs']['']['label'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $categories[] = $this->registry->create('Category', array($term, $scheme, $label)); + } + } + if (is_array($categories) && is_array($categories_parent)) + { + $categories = array_values(array_unique(array_merge($categories, $categories_parent))); + } + elseif (is_array($categories)) + { + $categories = array_values(array_unique($categories)); + } + elseif (is_array($categories_parent)) + { + $categories = array_values(array_unique($categories_parent)); + } + + // COPYRIGHTS + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['copyright'])) + { + $copyright_url = null; + $copyright_label = null; + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['copyright'][0]['attribs']['']['url'])) + { + $copyright_url = $this->sanitize($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['copyright'][0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['copyright'][0]['data'])) + { + $copyright_label = $this->sanitize($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['copyright'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $copyrights = $this->registry->create('Copyright', array($copyright_url, $copyright_label)); + } + elseif (isset($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['copyright'])) + { + $copyright_url = null; + $copyright_label = null; + if (isset($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['copyright'][0]['attribs']['']['url'])) + { + $copyright_url = $this->sanitize($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['copyright'][0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['copyright'][0]['data'])) + { + $copyright_label = $this->sanitize($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['copyright'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $copyrights = $this->registry->create('Copyright', array($copyright_url, $copyright_label)); + } + else + { + $copyrights = $copyrights_parent; + } + + // CREDITS + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['credit'])) + { + foreach ($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['credit'] as $credit) + { + $credit_role = null; + $credit_scheme = null; + $credit_name = null; + if (isset($credit['attribs']['']['role'])) + { + $credit_role = $this->sanitize($credit['attribs']['']['role'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($credit['attribs']['']['scheme'])) + { + $credit_scheme = $this->sanitize($credit['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $credit_scheme = 'urn:ebu'; + } + if (isset($credit['data'])) + { + $credit_name = $this->sanitize($credit['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $credits[] = $this->registry->create('Credit', array($credit_role, $credit_scheme, $credit_name)); + } + if (is_array($credits)) + { + $credits = array_values(array_unique($credits)); + } + } + elseif (isset($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['credit'])) + { + foreach ($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['credit'] as $credit) + { + $credit_role = null; + $credit_scheme = null; + $credit_name = null; + if (isset($credit['attribs']['']['role'])) + { + $credit_role = $this->sanitize($credit['attribs']['']['role'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($credit['attribs']['']['scheme'])) + { + $credit_scheme = $this->sanitize($credit['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $credit_scheme = 'urn:ebu'; + } + if (isset($credit['data'])) + { + $credit_name = $this->sanitize($credit['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $credits[] = $this->registry->create('Credit', array($credit_role, $credit_scheme, $credit_name)); + } + if (is_array($credits)) + { + $credits = array_values(array_unique($credits)); + } + } + else + { + $credits = $credits_parent; + } + + // DESCRIPTION + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['description'])) + { + $description = $this->sanitize($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['description'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif (isset($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['description'])) + { + $description = $this->sanitize($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['description'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $description = $description_parent; + } + + // HASHES + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['hash'])) + { + foreach ($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['hash'] as $hash) + { + $value = null; + $algo = null; + if (isset($hash['data'])) + { + $value = $this->sanitize($hash['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($hash['attribs']['']['algo'])) + { + $algo = $this->sanitize($hash['attribs']['']['algo'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $algo = 'md5'; + } + $hashes[] = $algo.':'.$value; + } + if (is_array($hashes)) + { + $hashes = array_values(array_unique($hashes)); + } + } + elseif (isset($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['hash'])) + { + foreach ($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['hash'] as $hash) + { + $value = null; + $algo = null; + if (isset($hash['data'])) + { + $value = $this->sanitize($hash['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($hash['attribs']['']['algo'])) + { + $algo = $this->sanitize($hash['attribs']['']['algo'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $algo = 'md5'; + } + $hashes[] = $algo.':'.$value; + } + if (is_array($hashes)) + { + $hashes = array_values(array_unique($hashes)); + } + } + else + { + $hashes = $hashes_parent; + } + + // KEYWORDS + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['keywords'])) + { + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['keywords'][0]['data'])) + { + $temp = explode(',', $this->sanitize($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['keywords'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT)); + foreach ($temp as $word) + { + $keywords[] = trim($word); + } + unset($temp); + } + if (is_array($keywords)) + { + $keywords = array_values(array_unique($keywords)); + } + } + elseif (isset($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['keywords'])) + { + if (isset($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['keywords'][0]['data'])) + { + $temp = explode(',', $this->sanitize($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['keywords'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT)); + foreach ($temp as $word) + { + $keywords[] = trim($word); + } + unset($temp); + } + if (is_array($keywords)) + { + $keywords = array_values(array_unique($keywords)); + } + } + else + { + $keywords = $keywords_parent; + } + + // PLAYER + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['player'])) + { + $player = $this->sanitize($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['player'][0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI); + } + elseif (isset($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['player'])) + { + $player = $this->sanitize($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['player'][0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI); + } + else + { + $player = $player_parent; + } + + // RATINGS + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['rating'])) + { + foreach ($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['rating'] as $rating) + { + $rating_scheme = null; + $rating_value = null; + if (isset($rating['attribs']['']['scheme'])) + { + $rating_scheme = $this->sanitize($rating['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $rating_scheme = 'urn:simple'; + } + if (isset($rating['data'])) + { + $rating_value = $this->sanitize($rating['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $ratings[] = $this->registry->create('Rating', array($rating_scheme, $rating_value)); + } + if (is_array($ratings)) + { + $ratings = array_values(array_unique($ratings)); + } + } + elseif (isset($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['rating'])) + { + foreach ($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['rating'] as $rating) + { + $rating_scheme = null; + $rating_value = null; + if (isset($rating['attribs']['']['scheme'])) + { + $rating_scheme = $this->sanitize($rating['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $rating_scheme = 'urn:simple'; + } + if (isset($rating['data'])) + { + $rating_value = $this->sanitize($rating['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $ratings[] = $this->registry->create('Rating', array($rating_scheme, $rating_value)); + } + if (is_array($ratings)) + { + $ratings = array_values(array_unique($ratings)); + } + } + else + { + $ratings = $ratings_parent; + } + + // RESTRICTIONS + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['restriction'])) + { + foreach ($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['restriction'] as $restriction) + { + $restriction_relationship = null; + $restriction_type = null; + $restriction_value = null; + if (isset($restriction['attribs']['']['relationship'])) + { + $restriction_relationship = $this->sanitize($restriction['attribs']['']['relationship'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($restriction['attribs']['']['type'])) + { + $restriction_type = $this->sanitize($restriction['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($restriction['data'])) + { + $restriction_value = $this->sanitize($restriction['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $restrictions[] = $this->registry->create('Restriction', array($restriction_relationship, $restriction_type, $restriction_value)); + } + if (is_array($restrictions)) + { + $restrictions = array_values(array_unique($restrictions)); + } + } + elseif (isset($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['restriction'])) + { + foreach ($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['restriction'] as $restriction) + { + $restriction_relationship = null; + $restriction_type = null; + $restriction_value = null; + if (isset($restriction['attribs']['']['relationship'])) + { + $restriction_relationship = $this->sanitize($restriction['attribs']['']['relationship'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($restriction['attribs']['']['type'])) + { + $restriction_type = $this->sanitize($restriction['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($restriction['data'])) + { + $restriction_value = $this->sanitize($restriction['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $restrictions[] = $this->registry->create('Restriction', array($restriction_relationship, $restriction_type, $restriction_value)); + } + if (is_array($restrictions)) + { + $restrictions = array_values(array_unique($restrictions)); + } + } + else + { + $restrictions = $restrictions_parent; + } + + // THUMBNAILS + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['thumbnail'])) + { + foreach ($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['thumbnail'] as $thumbnail) + { + $thumbnails[] = $this->sanitize($thumbnail['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI); + } + if (is_array($thumbnails)) + { + $thumbnails = array_values(array_unique($thumbnails)); + } + } + elseif (isset($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['thumbnail'])) + { + foreach ($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['thumbnail'] as $thumbnail) + { + $thumbnails[] = $this->sanitize($thumbnail['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI); + } + if (is_array($thumbnails)) + { + $thumbnails = array_values(array_unique($thumbnails)); + } + } + else + { + $thumbnails = $thumbnails_parent; + } + + // TITLES + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['title'])) + { + $title = $this->sanitize($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['title'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif (isset($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['title'])) + { + $title = $this->sanitize($group['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['title'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $title = $title_parent; + } + + $this->data['enclosures'][] = $this->registry->create('Enclosure', array($url, $type, $length, null, $bitrate, $captions, $categories, $channels, $copyrights, $credits, $description, $duration, $expression, $framerate, $hashes, $height, $keywords, $lang, $medium, $player, $ratings, $restrictions, $samplingrate, $thumbnails, $title, $width)); + } + } + } + } + + // If we have standalone media:content tags, loop through them. + if (isset($this->data['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['content'])) + { + foreach ((array) $this->data['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['content'] as $content) + { + if (isset($content['attribs']['']['url']) || isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['player'])) + { + // Attributes + $bitrate = null; + $channels = null; + $duration = null; + $expression = null; + $framerate = null; + $height = null; + $javascript = null; + $lang = null; + $length = null; + $medium = null; + $samplingrate = null; + $type = null; + $url = null; + $width = null; + + // Elements + $captions = null; + $categories = null; + $copyrights = null; + $credits = null; + $description = null; + $hashes = null; + $keywords = null; + $player = null; + $ratings = null; + $restrictions = null; + $thumbnails = null; + $title = null; + + // Start checking the attributes of media:content + if (isset($content['attribs']['']['bitrate'])) + { + $bitrate = $this->sanitize($content['attribs']['']['bitrate'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['channels'])) + { + $channels = $this->sanitize($content['attribs']['']['channels'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['duration'])) + { + $duration = $this->sanitize($content['attribs']['']['duration'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $duration = $duration_parent; + } + if (isset($content['attribs']['']['expression'])) + { + $expression = $this->sanitize($content['attribs']['']['expression'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['framerate'])) + { + $framerate = $this->sanitize($content['attribs']['']['framerate'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['height'])) + { + $height = $this->sanitize($content['attribs']['']['height'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['lang'])) + { + $lang = $this->sanitize($content['attribs']['']['lang'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['fileSize'])) + { + $length = ceil($content['attribs']['']['fileSize']); + } + if (isset($content['attribs']['']['medium'])) + { + $medium = $this->sanitize($content['attribs']['']['medium'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['samplingrate'])) + { + $samplingrate = $this->sanitize($content['attribs']['']['samplingrate'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['type'])) + { + $type = $this->sanitize($content['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['width'])) + { + $width = $this->sanitize($content['attribs']['']['width'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['attribs']['']['url'])) + { + $url = $this->sanitize($content['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI); + } + // Checking the other optional media: elements. Priority: media:content, media:group, item, channel + + // CAPTIONS + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['text'])) + { + foreach ($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['text'] as $caption) + { + $caption_type = null; + $caption_lang = null; + $caption_startTime = null; + $caption_endTime = null; + $caption_text = null; + if (isset($caption['attribs']['']['type'])) + { + $caption_type = $this->sanitize($caption['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['lang'])) + { + $caption_lang = $this->sanitize($caption['attribs']['']['lang'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['start'])) + { + $caption_startTime = $this->sanitize($caption['attribs']['']['start'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['attribs']['']['end'])) + { + $caption_endTime = $this->sanitize($caption['attribs']['']['end'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($caption['data'])) + { + $caption_text = $this->sanitize($caption['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $captions[] = $this->registry->create('Caption', array($caption_type, $caption_lang, $caption_startTime, $caption_endTime, $caption_text)); + } + if (is_array($captions)) + { + $captions = array_values(array_unique($captions)); + } + } + else + { + $captions = $captions_parent; + } + + // CATEGORIES + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['category'])) + { + foreach ((array) $content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['category'] as $category) + { + $term = null; + $scheme = null; + $label = null; + if (isset($category['data'])) + { + $term = $this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['scheme'])) + { + $scheme = $this->sanitize($category['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $scheme = 'http://search.yahoo.com/mrss/category_schema'; + } + if (isset($category['attribs']['']['label'])) + { + $label = $this->sanitize($category['attribs']['']['label'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $categories[] = $this->registry->create('Category', array($term, $scheme, $label)); + } + } + if (is_array($categories) && is_array($categories_parent)) + { + $categories = array_values(array_unique(array_merge($categories, $categories_parent))); + } + elseif (is_array($categories)) + { + $categories = array_values(array_unique($categories)); + } + elseif (is_array($categories_parent)) + { + $categories = array_values(array_unique($categories_parent)); + } + else + { + $categories = null; + } + + // COPYRIGHTS + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['copyright'])) + { + $copyright_url = null; + $copyright_label = null; + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['copyright'][0]['attribs']['']['url'])) + { + $copyright_url = $this->sanitize($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['copyright'][0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['copyright'][0]['data'])) + { + $copyright_label = $this->sanitize($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['copyright'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $copyrights = $this->registry->create('Copyright', array($copyright_url, $copyright_label)); + } + else + { + $copyrights = $copyrights_parent; + } + + // CREDITS + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['credit'])) + { + foreach ($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['credit'] as $credit) + { + $credit_role = null; + $credit_scheme = null; + $credit_name = null; + if (isset($credit['attribs']['']['role'])) + { + $credit_role = $this->sanitize($credit['attribs']['']['role'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($credit['attribs']['']['scheme'])) + { + $credit_scheme = $this->sanitize($credit['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $credit_scheme = 'urn:ebu'; + } + if (isset($credit['data'])) + { + $credit_name = $this->sanitize($credit['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $credits[] = $this->registry->create('Credit', array($credit_role, $credit_scheme, $credit_name)); + } + if (is_array($credits)) + { + $credits = array_values(array_unique($credits)); + } + } + else + { + $credits = $credits_parent; + } + + // DESCRIPTION + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['description'])) + { + $description = $this->sanitize($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['description'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $description = $description_parent; + } + + // HASHES + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['hash'])) + { + foreach ($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['hash'] as $hash) + { + $value = null; + $algo = null; + if (isset($hash['data'])) + { + $value = $this->sanitize($hash['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($hash['attribs']['']['algo'])) + { + $algo = $this->sanitize($hash['attribs']['']['algo'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $algo = 'md5'; + } + $hashes[] = $algo.':'.$value; + } + if (is_array($hashes)) + { + $hashes = array_values(array_unique($hashes)); + } + } + else + { + $hashes = $hashes_parent; + } + + // KEYWORDS + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['keywords'])) + { + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['keywords'][0]['data'])) + { + $temp = explode(',', $this->sanitize($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['keywords'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT)); + foreach ($temp as $word) + { + $keywords[] = trim($word); + } + unset($temp); + } + if (is_array($keywords)) + { + $keywords = array_values(array_unique($keywords)); + } + } + else + { + $keywords = $keywords_parent; + } + + // PLAYER + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['player'])) + { + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['player'][0]['attribs']['']['url'])) { + $player = $this->sanitize($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['player'][0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI); + } + } + else + { + $player = $player_parent; + } + + // RATINGS + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['rating'])) + { + foreach ($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['rating'] as $rating) + { + $rating_scheme = null; + $rating_value = null; + if (isset($rating['attribs']['']['scheme'])) + { + $rating_scheme = $this->sanitize($rating['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $rating_scheme = 'urn:simple'; + } + if (isset($rating['data'])) + { + $rating_value = $this->sanitize($rating['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $ratings[] = $this->registry->create('Rating', array($rating_scheme, $rating_value)); + } + if (is_array($ratings)) + { + $ratings = array_values(array_unique($ratings)); + } + } + else + { + $ratings = $ratings_parent; + } + + // RESTRICTIONS + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['restriction'])) + { + foreach ($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['restriction'] as $restriction) + { + $restriction_relationship = null; + $restriction_type = null; + $restriction_value = null; + if (isset($restriction['attribs']['']['relationship'])) + { + $restriction_relationship = $this->sanitize($restriction['attribs']['']['relationship'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($restriction['attribs']['']['type'])) + { + $restriction_type = $this->sanitize($restriction['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($restriction['data'])) + { + $restriction_value = $this->sanitize($restriction['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $restrictions[] = $this->registry->create('Restriction', array($restriction_relationship, $restriction_type, $restriction_value)); + } + if (is_array($restrictions)) + { + $restrictions = array_values(array_unique($restrictions)); + } + } + else + { + $restrictions = $restrictions_parent; + } + + // THUMBNAILS + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['thumbnail'])) + { + foreach ($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['thumbnail'] as $thumbnail) + { + if (isset($thumbnail['attribs']['']['url'])) { + $thumbnails[] = $this->sanitize($thumbnail['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI); + } + } + if (is_array($thumbnails)) + { + $thumbnails = array_values(array_unique($thumbnails)); + } + } + else + { + $thumbnails = $thumbnails_parent; + } + + // TITLES + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['title'])) + { + $title = $this->sanitize($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['title'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $title = $title_parent; + } + + $this->data['enclosures'][] = $this->registry->create('Enclosure', array($url, $type, $length, null, $bitrate, $captions, $categories, $channels, $copyrights, $credits, $description, $duration, $expression, $framerate, $hashes, $height, $keywords, $lang, $medium, $player, $ratings, $restrictions, $samplingrate, $thumbnails, $title, $width)); + } + } + } + + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'link') as $link) + { + if (isset($link['attribs']['']['href']) && !empty($link['attribs']['']['rel']) && $link['attribs']['']['rel'] === 'enclosure') + { + // Attributes + $bitrate = null; + $channels = null; + $duration = null; + $expression = null; + $framerate = null; + $height = null; + $javascript = null; + $lang = null; + $length = null; + $medium = null; + $samplingrate = null; + $type = null; + $url = null; + $width = null; + + $url = $this->sanitize($link['attribs']['']['href'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($link)); + if (isset($link['attribs']['']['type'])) + { + $type = $this->sanitize($link['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($link['attribs']['']['length'])) + { + $length = ceil($link['attribs']['']['length']); + } + if (isset($link['attribs']['']['title'])) + { + $title = $this->sanitize($link['attribs']['']['title'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $title = $title_parent; + } + + // Since we don't have group or content for these, we'll just pass the '*_parent' variables directly to the constructor + $this->data['enclosures'][] = $this->registry->create('Enclosure', array($url, $type, $length, null, $bitrate, $captions_parent, $categories_parent, $channels, $copyrights_parent, $credits_parent, $description_parent, $duration_parent, $expression, $framerate, $hashes_parent, $height, $keywords_parent, $lang, $medium, $player_parent, $ratings_parent, $restrictions_parent, $samplingrate, $thumbnails_parent, $title, $width)); + } + } + + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'link') as $link) + { + if (isset($link['attribs']['']['href']) && !empty($link['attribs']['']['rel']) && $link['attribs']['']['rel'] === 'enclosure') + { + // Attributes + $bitrate = null; + $channels = null; + $duration = null; + $expression = null; + $framerate = null; + $height = null; + $javascript = null; + $lang = null; + $length = null; + $medium = null; + $samplingrate = null; + $type = null; + $url = null; + $width = null; + + $url = $this->sanitize($link['attribs']['']['href'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($link)); + if (isset($link['attribs']['']['type'])) + { + $type = $this->sanitize($link['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($link['attribs']['']['length'])) + { + $length = ceil($link['attribs']['']['length']); + } + + // Since we don't have group or content for these, we'll just pass the '*_parent' variables directly to the constructor + $this->data['enclosures'][] = $this->registry->create('Enclosure', array($url, $type, $length, null, $bitrate, $captions_parent, $categories_parent, $channels, $copyrights_parent, $credits_parent, $description_parent, $duration_parent, $expression, $framerate, $hashes_parent, $height, $keywords_parent, $lang, $medium, $player_parent, $ratings_parent, $restrictions_parent, $samplingrate, $thumbnails_parent, $title_parent, $width)); + } + } + + if ($enclosure = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'enclosure')) + { + if (isset($enclosure[0]['attribs']['']['url'])) + { + // Attributes + $bitrate = null; + $channels = null; + $duration = null; + $expression = null; + $framerate = null; + $height = null; + $javascript = null; + $lang = null; + $length = null; + $medium = null; + $samplingrate = null; + $type = null; + $url = null; + $width = null; + + $url = $this->sanitize($enclosure[0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($enclosure[0])); + if (isset($enclosure[0]['attribs']['']['type'])) + { + $type = $this->sanitize($enclosure[0]['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($enclosure[0]['attribs']['']['length'])) + { + $length = ceil($enclosure[0]['attribs']['']['length']); + } + + // Since we don't have group or content for these, we'll just pass the '*_parent' variables directly to the constructor + $this->data['enclosures'][] = $this->registry->create('Enclosure', array($url, $type, $length, null, $bitrate, $captions_parent, $categories_parent, $channels, $copyrights_parent, $credits_parent, $description_parent, $duration_parent, $expression, $framerate, $hashes_parent, $height, $keywords_parent, $lang, $medium, $player_parent, $ratings_parent, $restrictions_parent, $samplingrate, $thumbnails_parent, $title_parent, $width)); + } + } + + if (sizeof($this->data['enclosures']) === 0 && ($url || $type || $length || $bitrate || $captions_parent || $categories_parent || $channels || $copyrights_parent || $credits_parent || $description_parent || $duration_parent || $expression || $framerate || $hashes_parent || $height || $keywords_parent || $lang || $medium || $player_parent || $ratings_parent || $restrictions_parent || $samplingrate || $thumbnails_parent || $title_parent || $width)) + { + // Since we don't have group or content for these, we'll just pass the '*_parent' variables directly to the constructor + $this->data['enclosures'][] = $this->registry->create('Enclosure', array($url, $type, $length, null, $bitrate, $captions_parent, $categories_parent, $channels, $copyrights_parent, $credits_parent, $description_parent, $duration_parent, $expression, $framerate, $hashes_parent, $height, $keywords_parent, $lang, $medium, $player_parent, $ratings_parent, $restrictions_parent, $samplingrate, $thumbnails_parent, $title_parent, $width)); + } + + $this->data['enclosures'] = array_values(array_unique($this->data['enclosures'])); + } + if (!empty($this->data['enclosures'])) + { + return $this->data['enclosures']; + } + else + { + return null; + } + } + + /** + * Get the latitude coordinates for the item + * + * Compatible with the W3C WGS84 Basic Geo and GeoRSS specifications + * + * Uses `<geo:lat>` or `<georss:point>` + * + * @since 1.0 + * @link http://www.w3.org/2003/01/geo/ W3C WGS84 Basic Geo + * @link http://www.georss.org/ GeoRSS + * @return string|null + */ + public function get_latitude() + { + if ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_W3C_BASIC_GEO, 'lat')) + { + return (float) $return[0]['data']; + } + elseif (($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_GEORSS, 'point')) && preg_match('/^((?:-)?[0-9]+(?:\.[0-9]+)) ((?:-)?[0-9]+(?:\.[0-9]+))$/', trim($return[0]['data']), $match)) + { + return (float) $match[1]; + } + else + { + return null; + } + } + + /** + * Get the longitude coordinates for the item + * + * Compatible with the W3C WGS84 Basic Geo and GeoRSS specifications + * + * Uses `<geo:long>`, `<geo:lon>` or `<georss:point>` + * + * @since 1.0 + * @link http://www.w3.org/2003/01/geo/ W3C WGS84 Basic Geo + * @link http://www.georss.org/ GeoRSS + * @return string|null + */ + public function get_longitude() + { + if ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_W3C_BASIC_GEO, 'long')) + { + return (float) $return[0]['data']; + } + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_W3C_BASIC_GEO, 'lon')) + { + return (float) $return[0]['data']; + } + elseif (($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_GEORSS, 'point')) && preg_match('/^((?:-)?[0-9]+(?:\.[0-9]+)) ((?:-)?[0-9]+(?:\.[0-9]+))$/', trim($return[0]['data']), $match)) + { + return (float) $match[2]; + } + else + { + return null; + } + } + + /** + * Get the `<atom:source>` for the item + * + * @since 1.1 + * @return SimplePie_Source|null + */ + public function get_source() + { + if ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'source')) + { + return $this->registry->create('Source', array($this, $return[0])); + } + else + { + return null; + } + } +} + diff --git a/vendor/simplepie/simplepie/library/SimplePie/Locator.php b/vendor/simplepie/simplepie/library/SimplePie/Locator.php new file mode 100644 index 000000000..bc314c2cd --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Locator.php @@ -0,0 +1,429 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Used for feed auto-discovery + * + * + * This class can be overloaded with {@see SimplePie::set_locator_class()} + * + * @package SimplePie + */ +class SimplePie_Locator +{ + var $useragent; + var $timeout; + var $file; + var $local = array(); + var $elsewhere = array(); + var $cached_entities = array(); + var $http_base; + var $base; + var $base_location = 0; + var $checked_feeds = 0; + var $max_checked_feeds = 10; + protected $registry; + + public function __construct(SimplePie_File $file, $timeout = 10, $useragent = null, $max_checked_feeds = 10) + { + $this->file = $file; + $this->useragent = $useragent; + $this->timeout = $timeout; + $this->max_checked_feeds = $max_checked_feeds; + + if (class_exists('DOMDocument')) + { + $this->dom = new DOMDocument(); + + set_error_handler(array('SimplePie_Misc', 'silence_errors')); + $this->dom->loadHTML($this->file->body); + restore_error_handler(); + } + else + { + $this->dom = null; + } + } + + public function set_registry(SimplePie_Registry $registry) + { + $this->registry = $registry; + } + + public function find($type = SIMPLEPIE_LOCATOR_ALL, &$working) + { + if ($this->is_feed($this->file)) + { + return $this->file; + } + + if ($this->file->method & SIMPLEPIE_FILE_SOURCE_REMOTE) + { + $sniffer = $this->registry->create('Content_Type_Sniffer', array($this->file)); + if ($sniffer->get_type() !== 'text/html') + { + return null; + } + } + + if ($type & ~SIMPLEPIE_LOCATOR_NONE) + { + $this->get_base(); + } + + if ($type & SIMPLEPIE_LOCATOR_AUTODISCOVERY && $working = $this->autodiscovery()) + { + return $working[0]; + } + + if ($type & (SIMPLEPIE_LOCATOR_LOCAL_EXTENSION | SIMPLEPIE_LOCATOR_LOCAL_BODY | SIMPLEPIE_LOCATOR_REMOTE_EXTENSION | SIMPLEPIE_LOCATOR_REMOTE_BODY) && $this->get_links()) + { + if ($type & SIMPLEPIE_LOCATOR_LOCAL_EXTENSION && $working = $this->extension($this->local)) + { + return $working[0]; + } + + if ($type & SIMPLEPIE_LOCATOR_LOCAL_BODY && $working = $this->body($this->local)) + { + return $working[0]; + } + + if ($type & SIMPLEPIE_LOCATOR_REMOTE_EXTENSION && $working = $this->extension($this->elsewhere)) + { + return $working[0]; + } + + if ($type & SIMPLEPIE_LOCATOR_REMOTE_BODY && $working = $this->body($this->elsewhere)) + { + return $working[0]; + } + } + return null; + } + + public function is_feed($file, $check_html = false) + { + if ($file->method & SIMPLEPIE_FILE_SOURCE_REMOTE) + { + $sniffer = $this->registry->create('Content_Type_Sniffer', array($file)); + $sniffed = $sniffer->get_type(); + $mime_types = array('application/rss+xml', 'application/rdf+xml', + 'text/rdf', 'application/atom+xml', 'text/xml', + 'application/xml', 'application/x-rss+xml'); + if ($check_html) + { + $mime_types[] = 'text/html'; + } + if (in_array($sniffed, $mime_types)) + { + return true; + } + else + { + return false; + } + } + elseif ($file->method & SIMPLEPIE_FILE_SOURCE_LOCAL) + { + return true; + } + else + { + return false; + } + } + + public function get_base() + { + if ($this->dom === null) + { + throw new SimplePie_Exception('DOMDocument not found, unable to use locator'); + } + $this->http_base = $this->file->url; + $this->base = $this->http_base; + $elements = $this->dom->getElementsByTagName('base'); + foreach ($elements as $element) + { + if ($element->hasAttribute('href')) + { + $base = $this->registry->call('Misc', 'absolutize_url', array(trim($element->getAttribute('href')), $this->http_base)); + if ($base === false) + { + continue; + } + $this->base = $base; + $this->base_location = method_exists($element, 'getLineNo') ? $element->getLineNo() : 0; + break; + } + } + } + + public function autodiscovery() + { + $done = array(); + $feeds = array(); + $feeds = array_merge($feeds, $this->search_elements_by_tag('link', $done, $feeds)); + $feeds = array_merge($feeds, $this->search_elements_by_tag('a', $done, $feeds)); + $feeds = array_merge($feeds, $this->search_elements_by_tag('area', $done, $feeds)); + + if (!empty($feeds)) + { + return array_values($feeds); + } + else + { + return null; + } + } + + protected function search_elements_by_tag($name, &$done, $feeds) + { + if ($this->dom === null) + { + throw new SimplePie_Exception('DOMDocument not found, unable to use locator'); + } + + $links = $this->dom->getElementsByTagName($name); + foreach ($links as $link) + { + if ($this->checked_feeds === $this->max_checked_feeds) + { + break; + } + if ($link->hasAttribute('href') && $link->hasAttribute('rel')) + { + $rel = array_unique($this->registry->call('Misc', 'space_separated_tokens', array(strtolower($link->getAttribute('rel'))))); + $line = method_exists($link, 'getLineNo') ? $link->getLineNo() : 1; + + if ($this->base_location < $line) + { + $href = $this->registry->call('Misc', 'absolutize_url', array(trim($link->getAttribute('href')), $this->base)); + } + else + { + $href = $this->registry->call('Misc', 'absolutize_url', array(trim($link->getAttribute('href')), $this->http_base)); + } + if ($href === false) + { + continue; + } + + if (!in_array($href, $done) && in_array('feed', $rel) || (in_array('alternate', $rel) && !in_array('stylesheet', $rel) && $link->hasAttribute('type') && in_array(strtolower($this->registry->call('Misc', 'parse_mime', array($link->getAttribute('type')))), array('text/html', 'application/rss+xml', 'application/atom+xml'))) && !isset($feeds[$href])) + { + $this->checked_feeds++; + $headers = array( + 'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1', + ); + $feed = $this->registry->create('File', array($href, $this->timeout, 5, $headers, $this->useragent)); + if ($feed->success && ($feed->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0 || ($feed->status_code === 200 || $feed->status_code > 206 && $feed->status_code < 300)) && $this->is_feed($feed, true)) + { + $feeds[$href] = $feed; + } + } + $done[] = $href; + } + } + + return $feeds; + } + + public function get_links() + { + if ($this->dom === null) + { + throw new SimplePie_Exception('DOMDocument not found, unable to use locator'); + } + + $links = $this->dom->getElementsByTagName('a'); + foreach ($links as $link) + { + if ($link->hasAttribute('href')) + { + $href = trim($link->getAttribute('href')); + $parsed = $this->registry->call('Misc', 'parse_url', array($href)); + if ($parsed['scheme'] === '' || preg_match('/^(https?|feed)?$/i', $parsed['scheme'])) + { + if (method_exists($link, 'getLineNo') && $this->base_location < $link->getLineNo()) + { + $href = $this->registry->call('Misc', 'absolutize_url', array(trim($link->getAttribute('href')), $this->base)); + } + else + { + $href = $this->registry->call('Misc', 'absolutize_url', array(trim($link->getAttribute('href')), $this->http_base)); + } + if ($href === false) + { + continue; + } + + $current = $this->registry->call('Misc', 'parse_url', array($this->file->url)); + + if ($parsed['authority'] === '' || $parsed['authority'] === $current['authority']) + { + $this->local[] = $href; + } + else + { + $this->elsewhere[] = $href; + } + } + } + } + $this->local = array_unique($this->local); + $this->elsewhere = array_unique($this->elsewhere); + if (!empty($this->local) || !empty($this->elsewhere)) + { + return true; + } + return null; + } + + public function get_rel_link($rel) + { + if ($this->dom === null) + { + throw new SimplePie_Exception('DOMDocument not found, unable to use '. + 'locator'); + } + if (!class_exists('DOMXpath')) + { + throw new SimplePie_Exception('DOMXpath not found, unable to use '. + 'get_rel_link'); + } + + $xpath = new DOMXpath($this->dom); + $query = '//a[@rel and @href] | //link[@rel and @href]'; + foreach ($xpath->query($query) as $link) + { + $href = trim($link->getAttribute('href')); + $parsed = $this->registry->call('Misc', 'parse_url', array($href)); + if ($parsed['scheme'] === '' || + preg_match('/^https?$/i', $parsed['scheme'])) + { + if (method_exists($link, 'getLineNo') && + $this->base_location < $link->getLineNo()) + { + $href = + $this->registry->call('Misc', 'absolutize_url', + array(trim($link->getAttribute('href')), + $this->base)); + } + else + { + $href = + $this->registry->call('Misc', 'absolutize_url', + array(trim($link->getAttribute('href')), + $this->http_base)); + } + if ($href === false) + { + return null; + } + $rel_values = explode(' ', strtolower($link->getAttribute('rel'))); + if (in_array($rel, $rel_values)) + { + return $href; + } + } + } + return null; + } + + public function extension(&$array) + { + foreach ($array as $key => $value) + { + if ($this->checked_feeds === $this->max_checked_feeds) + { + break; + } + if (in_array(strtolower(strrchr($value, '.')), array('.rss', '.rdf', '.atom', '.xml'))) + { + $this->checked_feeds++; + + $headers = array( + 'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1', + ); + $feed = $this->registry->create('File', array($value, $this->timeout, 5, $headers, $this->useragent)); + if ($feed->success && ($feed->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0 || ($feed->status_code === 200 || $feed->status_code > 206 && $feed->status_code < 300)) && $this->is_feed($feed)) + { + return array($feed); + } + else + { + unset($array[$key]); + } + } + } + return null; + } + + public function body(&$array) + { + foreach ($array as $key => $value) + { + if ($this->checked_feeds === $this->max_checked_feeds) + { + break; + } + if (preg_match('/(rss|rdf|atom|xml)/i', $value)) + { + $this->checked_feeds++; + $headers = array( + 'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1', + ); + $feed = $this->registry->create('File', array($value, $this->timeout, 5, null, $this->useragent)); + if ($feed->success && ($feed->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0 || ($feed->status_code === 200 || $feed->status_code > 206 && $feed->status_code < 300)) && $this->is_feed($feed)) + { + return array($feed); + } + else + { + unset($array[$key]); + } + } + } + return null; + } +} + diff --git a/vendor/simplepie/simplepie/library/SimplePie/Misc.php b/vendor/simplepie/simplepie/library/SimplePie/Misc.php new file mode 100644 index 000000000..2e3107eb4 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Misc.php @@ -0,0 +1,2279 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Miscellanous utilities + * + * @package SimplePie + */ +class SimplePie_Misc +{ + public static function time_hms($seconds) + { + $time = ''; + + $hours = floor($seconds / 3600); + $remainder = $seconds % 3600; + if ($hours > 0) + { + $time .= $hours.':'; + } + + $minutes = floor($remainder / 60); + $seconds = $remainder % 60; + if ($minutes < 10 && $hours > 0) + { + $minutes = '0' . $minutes; + } + if ($seconds < 10) + { + $seconds = '0' . $seconds; + } + + $time .= $minutes.':'; + $time .= $seconds; + + return $time; + } + + public static function absolutize_url($relative, $base) + { + $iri = SimplePie_IRI::absolutize(new SimplePie_IRI($base), $relative); + if ($iri === false) + { + return false; + } + return $iri->get_uri(); + } + + /** + * Get a HTML/XML element from a HTML string + * + * @deprecated Use DOMDocument instead (parsing HTML with regex is bad!) + * @param string $realname Element name (including namespace prefix if applicable) + * @param string $string HTML document + * @return array + */ + public static function get_element($realname, $string) + { + $return = array(); + $name = preg_quote($realname, '/'); + if (preg_match_all("/<($name)" . SIMPLEPIE_PCRE_HTML_ATTRIBUTE . "(>(.*)<\/$name>|(\/)?>)/siU", $string, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) + { + for ($i = 0, $total_matches = count($matches); $i < $total_matches; $i++) + { + $return[$i]['tag'] = $realname; + $return[$i]['full'] = $matches[$i][0][0]; + $return[$i]['offset'] = $matches[$i][0][1]; + if (strlen($matches[$i][3][0]) <= 2) + { + $return[$i]['self_closing'] = true; + } + else + { + $return[$i]['self_closing'] = false; + $return[$i]['content'] = $matches[$i][4][0]; + } + $return[$i]['attribs'] = array(); + if (isset($matches[$i][2][0]) && preg_match_all('/[\x09\x0A\x0B\x0C\x0D\x20]+([^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3E][^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3D\x3E]*)(?:[\x09\x0A\x0B\x0C\x0D\x20]*=[\x09\x0A\x0B\x0C\x0D\x20]*(?:"([^"]*)"|\'([^\']*)\'|([^\x09\x0A\x0B\x0C\x0D\x20\x22\x27\x3E][^\x09\x0A\x0B\x0C\x0D\x20\x3E]*)?))?/', ' ' . $matches[$i][2][0] . ' ', $attribs, PREG_SET_ORDER)) + { + for ($j = 0, $total_attribs = count($attribs); $j < $total_attribs; $j++) + { + if (count($attribs[$j]) === 2) + { + $attribs[$j][2] = $attribs[$j][1]; + } + $return[$i]['attribs'][strtolower($attribs[$j][1])]['data'] = SimplePie_Misc::entities_decode(end($attribs[$j])); + } + } + } + } + return $return; + } + + public static function element_implode($element) + { + $full = "<$element[tag]"; + foreach ($element['attribs'] as $key => $value) + { + $key = strtolower($key); + $full .= " $key=\"" . htmlspecialchars($value['data'], ENT_COMPAT, 'UTF-8') . '"'; + } + if ($element['self_closing']) + { + $full .= ' />'; + } + else + { + $full .= ">$element[content]</$element[tag]>"; + } + return $full; + } + + public static function error($message, $level, $file, $line) + { + if ((ini_get('error_reporting') & $level) > 0) + { + switch ($level) + { + case E_USER_ERROR: + $note = 'PHP Error'; + break; + case E_USER_WARNING: + $note = 'PHP Warning'; + break; + case E_USER_NOTICE: + $note = 'PHP Notice'; + break; + default: + $note = 'Unknown Error'; + break; + } + + $log_error = true; + if (!function_exists('error_log')) + { + $log_error = false; + } + + $log_file = @ini_get('error_log'); + if (!empty($log_file) && ('syslog' !== $log_file) && !@is_writable($log_file)) + { + $log_error = false; + } + + if ($log_error) + { + @error_log("$note: $message in $file on line $line", 0); + } + } + + return $message; + } + + public static function fix_protocol($url, $http = 1) + { + $url = SimplePie_Misc::normalize_url($url); + $parsed = SimplePie_Misc::parse_url($url); + if ($parsed['scheme'] !== '' && $parsed['scheme'] !== 'http' && $parsed['scheme'] !== 'https') + { + return SimplePie_Misc::fix_protocol(SimplePie_Misc::compress_parse_url('http', $parsed['authority'], $parsed['path'], $parsed['query'], $parsed['fragment']), $http); + } + + if ($parsed['scheme'] === '' && $parsed['authority'] === '' && !file_exists($url)) + { + return SimplePie_Misc::fix_protocol(SimplePie_Misc::compress_parse_url('http', $parsed['path'], '', $parsed['query'], $parsed['fragment']), $http); + } + + if ($http === 2 && $parsed['scheme'] !== '') + { + return "feed:$url"; + } + elseif ($http === 3 && strtolower($parsed['scheme']) === 'http') + { + return substr_replace($url, 'podcast', 0, 4); + } + elseif ($http === 4 && strtolower($parsed['scheme']) === 'http') + { + return substr_replace($url, 'itpc', 0, 4); + } + else + { + return $url; + } + } + + public static function array_merge_recursive($array1, $array2) + { + foreach ($array2 as $key => $value) + { + if (is_array($value)) + { + $array1[$key] = SimplePie_Misc::array_merge_recursive($array1[$key], $value); + } + else + { + $array1[$key] = $value; + } + } + + return $array1; + } + + public static function parse_url($url) + { + $iri = new SimplePie_IRI($url); + return array( + 'scheme' => (string) $iri->scheme, + 'authority' => (string) $iri->authority, + 'path' => (string) $iri->path, + 'query' => (string) $iri->query, + 'fragment' => (string) $iri->fragment + ); + } + + public static function compress_parse_url($scheme = '', $authority = '', $path = '', $query = '', $fragment = '') + { + $iri = new SimplePie_IRI(''); + $iri->scheme = $scheme; + $iri->authority = $authority; + $iri->path = $path; + $iri->query = $query; + $iri->fragment = $fragment; + return $iri->get_uri(); + } + + public static function normalize_url($url) + { + $iri = new SimplePie_IRI($url); + return $iri->get_uri(); + } + + public static function percent_encoding_normalization($match) + { + $integer = hexdec($match[1]); + if ($integer >= 0x41 && $integer <= 0x5A || $integer >= 0x61 && $integer <= 0x7A || $integer >= 0x30 && $integer <= 0x39 || $integer === 0x2D || $integer === 0x2E || $integer === 0x5F || $integer === 0x7E) + { + return chr($integer); + } + else + { + return strtoupper($match[0]); + } + } + + /** + * Converts a Windows-1252 encoded string to a UTF-8 encoded string + * + * @static + * @param string $string Windows-1252 encoded string + * @return string UTF-8 encoded string + */ + public static function windows_1252_to_utf8($string) + { + static $convert_table = array("\x80" => "\xE2\x82\xAC", "\x81" => "\xEF\xBF\xBD", "\x82" => "\xE2\x80\x9A", "\x83" => "\xC6\x92", "\x84" => "\xE2\x80\x9E", "\x85" => "\xE2\x80\xA6", "\x86" => "\xE2\x80\xA0", "\x87" => "\xE2\x80\xA1", "\x88" => "\xCB\x86", "\x89" => "\xE2\x80\xB0", "\x8A" => "\xC5\xA0", "\x8B" => "\xE2\x80\xB9", "\x8C" => "\xC5\x92", "\x8D" => "\xEF\xBF\xBD", "\x8E" => "\xC5\xBD", "\x8F" => "\xEF\xBF\xBD", "\x90" => "\xEF\xBF\xBD", "\x91" => "\xE2\x80\x98", "\x92" => "\xE2\x80\x99", "\x93" => "\xE2\x80\x9C", "\x94" => "\xE2\x80\x9D", "\x95" => "\xE2\x80\xA2", "\x96" => "\xE2\x80\x93", "\x97" => "\xE2\x80\x94", "\x98" => "\xCB\x9C", "\x99" => "\xE2\x84\xA2", "\x9A" => "\xC5\xA1", "\x9B" => "\xE2\x80\xBA", "\x9C" => "\xC5\x93", "\x9D" => "\xEF\xBF\xBD", "\x9E" => "\xC5\xBE", "\x9F" => "\xC5\xB8", "\xA0" => "\xC2\xA0", "\xA1" => "\xC2\xA1", "\xA2" => "\xC2\xA2", "\xA3" => "\xC2\xA3", "\xA4" => "\xC2\xA4", "\xA5" => "\xC2\xA5", "\xA6" => "\xC2\xA6", "\xA7" => "\xC2\xA7", "\xA8" => "\xC2\xA8", "\xA9" => "\xC2\xA9", "\xAA" => "\xC2\xAA", "\xAB" => "\xC2\xAB", "\xAC" => "\xC2\xAC", "\xAD" => "\xC2\xAD", "\xAE" => "\xC2\xAE", "\xAF" => "\xC2\xAF", "\xB0" => "\xC2\xB0", "\xB1" => "\xC2\xB1", "\xB2" => "\xC2\xB2", "\xB3" => "\xC2\xB3", "\xB4" => "\xC2\xB4", "\xB5" => "\xC2\xB5", "\xB6" => "\xC2\xB6", "\xB7" => "\xC2\xB7", "\xB8" => "\xC2\xB8", "\xB9" => "\xC2\xB9", "\xBA" => "\xC2\xBA", "\xBB" => "\xC2\xBB", "\xBC" => "\xC2\xBC", "\xBD" => "\xC2\xBD", "\xBE" => "\xC2\xBE", "\xBF" => "\xC2\xBF", "\xC0" => "\xC3\x80", "\xC1" => "\xC3\x81", "\xC2" => "\xC3\x82", "\xC3" => "\xC3\x83", "\xC4" => "\xC3\x84", "\xC5" => "\xC3\x85", "\xC6" => "\xC3\x86", "\xC7" => "\xC3\x87", "\xC8" => "\xC3\x88", "\xC9" => "\xC3\x89", "\xCA" => "\xC3\x8A", "\xCB" => "\xC3\x8B", "\xCC" => "\xC3\x8C", "\xCD" => "\xC3\x8D", "\xCE" => "\xC3\x8E", "\xCF" => "\xC3\x8F", "\xD0" => "\xC3\x90", "\xD1" => "\xC3\x91", "\xD2" => "\xC3\x92", "\xD3" => "\xC3\x93", "\xD4" => "\xC3\x94", "\xD5" => "\xC3\x95", "\xD6" => "\xC3\x96", "\xD7" => "\xC3\x97", "\xD8" => "\xC3\x98", "\xD9" => "\xC3\x99", "\xDA" => "\xC3\x9A", "\xDB" => "\xC3\x9B", "\xDC" => "\xC3\x9C", "\xDD" => "\xC3\x9D", "\xDE" => "\xC3\x9E", "\xDF" => "\xC3\x9F", "\xE0" => "\xC3\xA0", "\xE1" => "\xC3\xA1", "\xE2" => "\xC3\xA2", "\xE3" => "\xC3\xA3", "\xE4" => "\xC3\xA4", "\xE5" => "\xC3\xA5", "\xE6" => "\xC3\xA6", "\xE7" => "\xC3\xA7", "\xE8" => "\xC3\xA8", "\xE9" => "\xC3\xA9", "\xEA" => "\xC3\xAA", "\xEB" => "\xC3\xAB", "\xEC" => "\xC3\xAC", "\xED" => "\xC3\xAD", "\xEE" => "\xC3\xAE", "\xEF" => "\xC3\xAF", "\xF0" => "\xC3\xB0", "\xF1" => "\xC3\xB1", "\xF2" => "\xC3\xB2", "\xF3" => "\xC3\xB3", "\xF4" => "\xC3\xB4", "\xF5" => "\xC3\xB5", "\xF6" => "\xC3\xB6", "\xF7" => "\xC3\xB7", "\xF8" => "\xC3\xB8", "\xF9" => "\xC3\xB9", "\xFA" => "\xC3\xBA", "\xFB" => "\xC3\xBB", "\xFC" => "\xC3\xBC", "\xFD" => "\xC3\xBD", "\xFE" => "\xC3\xBE", "\xFF" => "\xC3\xBF"); + + return strtr($string, $convert_table); + } + + /** + * Change a string from one encoding to another + * + * @param string $data Raw data in $input encoding + * @param string $input Encoding of $data + * @param string $output Encoding you want + * @return string|boolean False if we can't convert it + */ + public static function change_encoding($data, $input, $output) + { + $input = SimplePie_Misc::encoding($input); + $output = SimplePie_Misc::encoding($output); + + // We fail to fail on non US-ASCII bytes + if ($input === 'US-ASCII') + { + static $non_ascii_octects = ''; + if (!$non_ascii_octects) + { + for ($i = 0x80; $i <= 0xFF; $i++) + { + $non_ascii_octects .= chr($i); + } + } + $data = substr($data, 0, strcspn($data, $non_ascii_octects)); + } + + // This is first, as behaviour of this is completely predictable + if ($input === 'windows-1252' && $output === 'UTF-8') + { + return SimplePie_Misc::windows_1252_to_utf8($data); + } + // This is second, as behaviour of this varies only with PHP version (the middle part of this expression checks the encoding is supported). + elseif (function_exists('mb_convert_encoding') && ($return = SimplePie_Misc::change_encoding_mbstring($data, $input, $output))) + { + return $return; + } + // This is third, as behaviour of this varies with OS userland and PHP version + elseif (function_exists('iconv') && ($return = SimplePie_Misc::change_encoding_iconv($data, $input, $output))) + { + return $return; + } + // This is last, as behaviour of this varies with OS userland and PHP version + elseif (class_exists('\UConverter') && ($return = SimplePie_Misc::change_encoding_uconverter($data, $input, $output))) + { + return $return; + } + // If we can't do anything, just fail + else + { + return false; + } + } + + protected static function change_encoding_mbstring($data, $input, $output) + { + if ($input === 'windows-949') + { + $input = 'EUC-KR'; + } + if ($output === 'windows-949') + { + $output = 'EUC-KR'; + } + if ($input === 'Windows-31J') + { + $input = 'SJIS'; + } + if ($output === 'Windows-31J') + { + $output = 'SJIS'; + } + + // Check that the encoding is supported + if (@mb_convert_encoding("\x80", 'UTF-16BE', $input) === "\x00\x80") + { + return false; + } + if (!in_array($input, mb_list_encodings())) + { + return false; + } + + // Let's do some conversion + if ($return = @mb_convert_encoding($data, $output, $input)) + { + return $return; + } + + return false; + } + + protected static function change_encoding_iconv($data, $input, $output) + { + return @iconv($input, $output, $data); + } + + /** + * @param string $data + * @param string $input + * @param string $output + * @return string|false + */ + protected static function change_encoding_uconverter($data, $input, $output) + { + return @\UConverter::transcode($data, $output, $input); + } + + /** + * Normalize an encoding name + * + * This is automatically generated by create.php + * + * To generate it, run `php create.php` on the command line, and copy the + * output to replace this function. + * + * @param string $charset Character set to standardise + * @return string Standardised name + */ + public static function encoding($charset) + { + // Normalization from UTS #22 + switch (strtolower(preg_replace('/(?:[^a-zA-Z0-9]+|([^0-9])0+)/', '\1', $charset))) + { + case 'adobestandardencoding': + case 'csadobestandardencoding': + return 'Adobe-Standard-Encoding'; + + case 'adobesymbolencoding': + case 'cshppsmath': + return 'Adobe-Symbol-Encoding'; + + case 'ami1251': + case 'amiga1251': + return 'Amiga-1251'; + + case 'ansix31101983': + case 'csat5001983': + case 'csiso99naplps': + case 'isoir99': + case 'naplps': + return 'ANSI_X3.110-1983'; + + case 'arabic7': + case 'asmo449': + case 'csiso89asmo449': + case 'iso9036': + case 'isoir89': + return 'ASMO_449'; + + case 'big5': + case 'csbig5': + return 'Big5'; + + case 'big5hkscs': + return 'Big5-HKSCS'; + + case 'bocu1': + case 'csbocu1': + return 'BOCU-1'; + + case 'brf': + case 'csbrf': + return 'BRF'; + + case 'bs4730': + case 'csiso4unitedkingdom': + case 'gb': + case 'iso646gb': + case 'isoir4': + case 'uk': + return 'BS_4730'; + + case 'bsviewdata': + case 'csiso47bsviewdata': + case 'isoir47': + return 'BS_viewdata'; + + case 'cesu8': + case 'cscesu8': + return 'CESU-8'; + + case 'ca': + case 'csa71': + case 'csaz243419851': + case 'csiso121canadian1': + case 'iso646ca': + case 'isoir121': + return 'CSA_Z243.4-1985-1'; + + case 'csa72': + case 'csaz243419852': + case 'csiso122canadian2': + case 'iso646ca2': + case 'isoir122': + return 'CSA_Z243.4-1985-2'; + + case 'csaz24341985gr': + case 'csiso123csaz24341985gr': + case 'isoir123': + return 'CSA_Z243.4-1985-gr'; + + case 'csiso139csn369103': + case 'csn369103': + case 'isoir139': + return 'CSN_369103'; + + case 'csdecmcs': + case 'dec': + case 'decmcs': + return 'DEC-MCS'; + + case 'csiso21german': + case 'de': + case 'din66003': + case 'iso646de': + case 'isoir21': + return 'DIN_66003'; + + case 'csdkus': + case 'dkus': + return 'dk-us'; + + case 'csiso646danish': + case 'dk': + case 'ds2089': + case 'iso646dk': + return 'DS_2089'; + + case 'csibmebcdicatde': + case 'ebcdicatde': + return 'EBCDIC-AT-DE'; + + case 'csebcdicatdea': + case 'ebcdicatdea': + return 'EBCDIC-AT-DE-A'; + + case 'csebcdiccafr': + case 'ebcdiccafr': + return 'EBCDIC-CA-FR'; + + case 'csebcdicdkno': + case 'ebcdicdkno': + return 'EBCDIC-DK-NO'; + + case 'csebcdicdknoa': + case 'ebcdicdknoa': + return 'EBCDIC-DK-NO-A'; + + case 'csebcdices': + case 'ebcdices': + return 'EBCDIC-ES'; + + case 'csebcdicesa': + case 'ebcdicesa': + return 'EBCDIC-ES-A'; + + case 'csebcdicess': + case 'ebcdicess': + return 'EBCDIC-ES-S'; + + case 'csebcdicfise': + case 'ebcdicfise': + return 'EBCDIC-FI-SE'; + + case 'csebcdicfisea': + case 'ebcdicfisea': + return 'EBCDIC-FI-SE-A'; + + case 'csebcdicfr': + case 'ebcdicfr': + return 'EBCDIC-FR'; + + case 'csebcdicit': + case 'ebcdicit': + return 'EBCDIC-IT'; + + case 'csebcdicpt': + case 'ebcdicpt': + return 'EBCDIC-PT'; + + case 'csebcdicuk': + case 'ebcdicuk': + return 'EBCDIC-UK'; + + case 'csebcdicus': + case 'ebcdicus': + return 'EBCDIC-US'; + + case 'csiso111ecmacyrillic': + case 'ecmacyrillic': + case 'isoir111': + case 'koi8e': + return 'ECMA-cyrillic'; + + case 'csiso17spanish': + case 'es': + case 'iso646es': + case 'isoir17': + return 'ES'; + + case 'csiso85spanish2': + case 'es2': + case 'iso646es2': + case 'isoir85': + return 'ES2'; + + case 'cseucpkdfmtjapanese': + case 'eucjp': + case 'extendedunixcodepackedformatforjapanese': + return 'EUC-JP'; + + case 'cseucfixwidjapanese': + case 'extendedunixcodefixedwidthforjapanese': + return 'Extended_UNIX_Code_Fixed_Width_for_Japanese'; + + case 'gb18030': + return 'GB18030'; + + case 'chinese': + case 'cp936': + case 'csgb2312': + case 'csiso58gb231280': + case 'gb2312': + case 'gb231280': + case 'gbk': + case 'isoir58': + case 'ms936': + case 'windows936': + return 'GBK'; + + case 'cn': + case 'csiso57gb1988': + case 'gb198880': + case 'iso646cn': + case 'isoir57': + return 'GB_1988-80'; + + case 'csiso153gost1976874': + case 'gost1976874': + case 'isoir153': + case 'stsev35888': + return 'GOST_19768-74'; + + case 'csiso150': + case 'csiso150greekccitt': + case 'greekccitt': + case 'isoir150': + return 'greek-ccitt'; + + case 'csiso88greek7': + case 'greek7': + case 'isoir88': + return 'greek7'; + + case 'csiso18greek7old': + case 'greek7old': + case 'isoir18': + return 'greek7-old'; + + case 'cshpdesktop': + case 'hpdesktop': + return 'HP-DeskTop'; + + case 'cshplegal': + case 'hplegal': + return 'HP-Legal'; + + case 'cshpmath8': + case 'hpmath8': + return 'HP-Math8'; + + case 'cshppifont': + case 'hppifont': + return 'HP-Pi-font'; + + case 'cshproman8': + case 'hproman8': + case 'r8': + case 'roman8': + return 'hp-roman8'; + + case 'hzgb2312': + return 'HZ-GB-2312'; + + case 'csibmsymbols': + case 'ibmsymbols': + return 'IBM-Symbols'; + + case 'csibmthai': + case 'ibmthai': + return 'IBM-Thai'; + + case 'cp37': + case 'csibm37': + case 'ebcdiccpca': + case 'ebcdiccpnl': + case 'ebcdiccpus': + case 'ebcdiccpwt': + case 'ibm37': + return 'IBM037'; + + case 'cp38': + case 'csibm38': + case 'ebcdicint': + case 'ibm38': + return 'IBM038'; + + case 'cp273': + case 'csibm273': + case 'ibm273': + return 'IBM273'; + + case 'cp274': + case 'csibm274': + case 'ebcdicbe': + case 'ibm274': + return 'IBM274'; + + case 'cp275': + case 'csibm275': + case 'ebcdicbr': + case 'ibm275': + return 'IBM275'; + + case 'csibm277': + case 'ebcdiccpdk': + case 'ebcdiccpno': + case 'ibm277': + return 'IBM277'; + + case 'cp278': + case 'csibm278': + case 'ebcdiccpfi': + case 'ebcdiccpse': + case 'ibm278': + return 'IBM278'; + + case 'cp280': + case 'csibm280': + case 'ebcdiccpit': + case 'ibm280': + return 'IBM280'; + + case 'cp281': + case 'csibm281': + case 'ebcdicjpe': + case 'ibm281': + return 'IBM281'; + + case 'cp284': + case 'csibm284': + case 'ebcdiccpes': + case 'ibm284': + return 'IBM284'; + + case 'cp285': + case 'csibm285': + case 'ebcdiccpgb': + case 'ibm285': + return 'IBM285'; + + case 'cp290': + case 'csibm290': + case 'ebcdicjpkana': + case 'ibm290': + return 'IBM290'; + + case 'cp297': + case 'csibm297': + case 'ebcdiccpfr': + case 'ibm297': + return 'IBM297'; + + case 'cp420': + case 'csibm420': + case 'ebcdiccpar1': + case 'ibm420': + return 'IBM420'; + + case 'cp423': + case 'csibm423': + case 'ebcdiccpgr': + case 'ibm423': + return 'IBM423'; + + case 'cp424': + case 'csibm424': + case 'ebcdiccphe': + case 'ibm424': + return 'IBM424'; + + case '437': + case 'cp437': + case 'cspc8codepage437': + case 'ibm437': + return 'IBM437'; + + case 'cp500': + case 'csibm500': + case 'ebcdiccpbe': + case 'ebcdiccpch': + case 'ibm500': + return 'IBM500'; + + case 'cp775': + case 'cspc775baltic': + case 'ibm775': + return 'IBM775'; + + case '850': + case 'cp850': + case 'cspc850multilingual': + case 'ibm850': + return 'IBM850'; + + case '851': + case 'cp851': + case 'csibm851': + case 'ibm851': + return 'IBM851'; + + case '852': + case 'cp852': + case 'cspcp852': + case 'ibm852': + return 'IBM852'; + + case '855': + case 'cp855': + case 'csibm855': + case 'ibm855': + return 'IBM855'; + + case '857': + case 'cp857': + case 'csibm857': + case 'ibm857': + return 'IBM857'; + + case 'ccsid858': + case 'cp858': + case 'ibm858': + case 'pcmultilingual850euro': + return 'IBM00858'; + + case '860': + case 'cp860': + case 'csibm860': + case 'ibm860': + return 'IBM860'; + + case '861': + case 'cp861': + case 'cpis': + case 'csibm861': + case 'ibm861': + return 'IBM861'; + + case '862': + case 'cp862': + case 'cspc862latinhebrew': + case 'ibm862': + return 'IBM862'; + + case '863': + case 'cp863': + case 'csibm863': + case 'ibm863': + return 'IBM863'; + + case 'cp864': + case 'csibm864': + case 'ibm864': + return 'IBM864'; + + case '865': + case 'cp865': + case 'csibm865': + case 'ibm865': + return 'IBM865'; + + case '866': + case 'cp866': + case 'csibm866': + case 'ibm866': + return 'IBM866'; + + case 'cp868': + case 'cpar': + case 'csibm868': + case 'ibm868': + return 'IBM868'; + + case '869': + case 'cp869': + case 'cpgr': + case 'csibm869': + case 'ibm869': + return 'IBM869'; + + case 'cp870': + case 'csibm870': + case 'ebcdiccproece': + case 'ebcdiccpyu': + case 'ibm870': + return 'IBM870'; + + case 'cp871': + case 'csibm871': + case 'ebcdiccpis': + case 'ibm871': + return 'IBM871'; + + case 'cp880': + case 'csibm880': + case 'ebcdiccyrillic': + case 'ibm880': + return 'IBM880'; + + case 'cp891': + case 'csibm891': + case 'ibm891': + return 'IBM891'; + + case 'cp903': + case 'csibm903': + case 'ibm903': + return 'IBM903'; + + case '904': + case 'cp904': + case 'csibbm904': + case 'ibm904': + return 'IBM904'; + + case 'cp905': + case 'csibm905': + case 'ebcdiccptr': + case 'ibm905': + return 'IBM905'; + + case 'cp918': + case 'csibm918': + case 'ebcdiccpar2': + case 'ibm918': + return 'IBM918'; + + case 'ccsid924': + case 'cp924': + case 'ebcdiclatin9euro': + case 'ibm924': + return 'IBM00924'; + + case 'cp1026': + case 'csibm1026': + case 'ibm1026': + return 'IBM1026'; + + case 'ibm1047': + return 'IBM1047'; + + case 'ccsid1140': + case 'cp1140': + case 'ebcdicus37euro': + case 'ibm1140': + return 'IBM01140'; + + case 'ccsid1141': + case 'cp1141': + case 'ebcdicde273euro': + case 'ibm1141': + return 'IBM01141'; + + case 'ccsid1142': + case 'cp1142': + case 'ebcdicdk277euro': + case 'ebcdicno277euro': + case 'ibm1142': + return 'IBM01142'; + + case 'ccsid1143': + case 'cp1143': + case 'ebcdicfi278euro': + case 'ebcdicse278euro': + case 'ibm1143': + return 'IBM01143'; + + case 'ccsid1144': + case 'cp1144': + case 'ebcdicit280euro': + case 'ibm1144': + return 'IBM01144'; + + case 'ccsid1145': + case 'cp1145': + case 'ebcdices284euro': + case 'ibm1145': + return 'IBM01145'; + + case 'ccsid1146': + case 'cp1146': + case 'ebcdicgb285euro': + case 'ibm1146': + return 'IBM01146'; + + case 'ccsid1147': + case 'cp1147': + case 'ebcdicfr297euro': + case 'ibm1147': + return 'IBM01147'; + + case 'ccsid1148': + case 'cp1148': + case 'ebcdicinternational500euro': + case 'ibm1148': + return 'IBM01148'; + + case 'ccsid1149': + case 'cp1149': + case 'ebcdicis871euro': + case 'ibm1149': + return 'IBM01149'; + + case 'csiso143iecp271': + case 'iecp271': + case 'isoir143': + return 'IEC_P27-1'; + + case 'csiso49inis': + case 'inis': + case 'isoir49': + return 'INIS'; + + case 'csiso50inis8': + case 'inis8': + case 'isoir50': + return 'INIS-8'; + + case 'csiso51iniscyrillic': + case 'iniscyrillic': + case 'isoir51': + return 'INIS-cyrillic'; + + case 'csinvariant': + case 'invariant': + return 'INVARIANT'; + + case 'iso2022cn': + return 'ISO-2022-CN'; + + case 'iso2022cnext': + return 'ISO-2022-CN-EXT'; + + case 'csiso2022jp': + case 'iso2022jp': + return 'ISO-2022-JP'; + + case 'csiso2022jp2': + case 'iso2022jp2': + return 'ISO-2022-JP-2'; + + case 'csiso2022kr': + case 'iso2022kr': + return 'ISO-2022-KR'; + + case 'cswindows30latin1': + case 'iso88591windows30latin1': + return 'ISO-8859-1-Windows-3.0-Latin-1'; + + case 'cswindows31latin1': + case 'iso88591windows31latin1': + return 'ISO-8859-1-Windows-3.1-Latin-1'; + + case 'csisolatin2': + case 'iso88592': + case 'iso885921987': + case 'isoir101': + case 'l2': + case 'latin2': + return 'ISO-8859-2'; + + case 'cswindows31latin2': + case 'iso88592windowslatin2': + return 'ISO-8859-2-Windows-Latin-2'; + + case 'csisolatin3': + case 'iso88593': + case 'iso885931988': + case 'isoir109': + case 'l3': + case 'latin3': + return 'ISO-8859-3'; + + case 'csisolatin4': + case 'iso88594': + case 'iso885941988': + case 'isoir110': + case 'l4': + case 'latin4': + return 'ISO-8859-4'; + + case 'csisolatincyrillic': + case 'cyrillic': + case 'iso88595': + case 'iso885951988': + case 'isoir144': + return 'ISO-8859-5'; + + case 'arabic': + case 'asmo708': + case 'csisolatinarabic': + case 'ecma114': + case 'iso88596': + case 'iso885961987': + case 'isoir127': + return 'ISO-8859-6'; + + case 'csiso88596e': + case 'iso88596e': + return 'ISO-8859-6-E'; + + case 'csiso88596i': + case 'iso88596i': + return 'ISO-8859-6-I'; + + case 'csisolatingreek': + case 'ecma118': + case 'elot928': + case 'greek': + case 'greek8': + case 'iso88597': + case 'iso885971987': + case 'isoir126': + return 'ISO-8859-7'; + + case 'csisolatinhebrew': + case 'hebrew': + case 'iso88598': + case 'iso885981988': + case 'isoir138': + return 'ISO-8859-8'; + + case 'csiso88598e': + case 'iso88598e': + return 'ISO-8859-8-E'; + + case 'csiso88598i': + case 'iso88598i': + return 'ISO-8859-8-I'; + + case 'cswindows31latin5': + case 'iso88599windowslatin5': + return 'ISO-8859-9-Windows-Latin-5'; + + case 'csisolatin6': + case 'iso885910': + case 'iso8859101992': + case 'isoir157': + case 'l6': + case 'latin6': + return 'ISO-8859-10'; + + case 'iso885913': + return 'ISO-8859-13'; + + case 'iso885914': + case 'iso8859141998': + case 'isoceltic': + case 'isoir199': + case 'l8': + case 'latin8': + return 'ISO-8859-14'; + + case 'iso885915': + case 'latin9': + return 'ISO-8859-15'; + + case 'iso885916': + case 'iso8859162001': + case 'isoir226': + case 'l10': + case 'latin10': + return 'ISO-8859-16'; + + case 'iso10646j1': + return 'ISO-10646-J-1'; + + case 'csunicode': + case 'iso10646ucs2': + return 'ISO-10646-UCS-2'; + + case 'csucs4': + case 'iso10646ucs4': + return 'ISO-10646-UCS-4'; + + case 'csunicodeascii': + case 'iso10646ucsbasic': + return 'ISO-10646-UCS-Basic'; + + case 'csunicodelatin1': + case 'iso10646': + case 'iso10646unicodelatin1': + return 'ISO-10646-Unicode-Latin1'; + + case 'csiso10646utf1': + case 'iso10646utf1': + return 'ISO-10646-UTF-1'; + + case 'csiso115481': + case 'iso115481': + case 'isotr115481': + return 'ISO-11548-1'; + + case 'csiso90': + case 'isoir90': + return 'iso-ir-90'; + + case 'csunicodeibm1261': + case 'isounicodeibm1261': + return 'ISO-Unicode-IBM-1261'; + + case 'csunicodeibm1264': + case 'isounicodeibm1264': + return 'ISO-Unicode-IBM-1264'; + + case 'csunicodeibm1265': + case 'isounicodeibm1265': + return 'ISO-Unicode-IBM-1265'; + + case 'csunicodeibm1268': + case 'isounicodeibm1268': + return 'ISO-Unicode-IBM-1268'; + + case 'csunicodeibm1276': + case 'isounicodeibm1276': + return 'ISO-Unicode-IBM-1276'; + + case 'csiso646basic1983': + case 'iso646basic1983': + case 'ref': + return 'ISO_646.basic:1983'; + + case 'csiso2intlrefversion': + case 'irv': + case 'iso646irv1983': + case 'isoir2': + return 'ISO_646.irv:1983'; + + case 'csiso2033': + case 'e13b': + case 'iso20331983': + case 'isoir98': + return 'ISO_2033-1983'; + + case 'csiso5427cyrillic': + case 'iso5427': + case 'isoir37': + return 'ISO_5427'; + + case 'iso5427cyrillic1981': + case 'iso54271981': + case 'isoir54': + return 'ISO_5427:1981'; + + case 'csiso5428greek': + case 'iso54281980': + case 'isoir55': + return 'ISO_5428:1980'; + + case 'csiso6937add': + case 'iso6937225': + case 'isoir152': + return 'ISO_6937-2-25'; + + case 'csisotextcomm': + case 'iso69372add': + case 'isoir142': + return 'ISO_6937-2-add'; + + case 'csiso8859supp': + case 'iso8859supp': + case 'isoir154': + case 'latin125': + return 'ISO_8859-supp'; + + case 'csiso10367box': + case 'iso10367box': + case 'isoir155': + return 'ISO_10367-box'; + + case 'csiso15italian': + case 'iso646it': + case 'isoir15': + case 'it': + return 'IT'; + + case 'csiso13jisc6220jp': + case 'isoir13': + case 'jisc62201969': + case 'jisc62201969jp': + case 'katakana': + case 'x2017': + return 'JIS_C6220-1969-jp'; + + case 'csiso14jisc6220ro': + case 'iso646jp': + case 'isoir14': + case 'jisc62201969ro': + case 'jp': + return 'JIS_C6220-1969-ro'; + + case 'csiso42jisc62261978': + case 'isoir42': + case 'jisc62261978': + return 'JIS_C6226-1978'; + + case 'csiso87jisx208': + case 'isoir87': + case 'jisc62261983': + case 'jisx2081983': + case 'x208': + return 'JIS_C6226-1983'; + + case 'csiso91jisc62291984a': + case 'isoir91': + case 'jisc62291984a': + case 'jpocra': + return 'JIS_C6229-1984-a'; + + case 'csiso92jisc62991984b': + case 'iso646jpocrb': + case 'isoir92': + case 'jisc62291984b': + case 'jpocrb': + return 'JIS_C6229-1984-b'; + + case 'csiso93jis62291984badd': + case 'isoir93': + case 'jisc62291984badd': + case 'jpocrbadd': + return 'JIS_C6229-1984-b-add'; + + case 'csiso94jis62291984hand': + case 'isoir94': + case 'jisc62291984hand': + case 'jpocrhand': + return 'JIS_C6229-1984-hand'; + + case 'csiso95jis62291984handadd': + case 'isoir95': + case 'jisc62291984handadd': + case 'jpocrhandadd': + return 'JIS_C6229-1984-hand-add'; + + case 'csiso96jisc62291984kana': + case 'isoir96': + case 'jisc62291984kana': + return 'JIS_C6229-1984-kana'; + + case 'csjisencoding': + case 'jisencoding': + return 'JIS_Encoding'; + + case 'cshalfwidthkatakana': + case 'jisx201': + case 'x201': + return 'JIS_X0201'; + + case 'csiso159jisx2121990': + case 'isoir159': + case 'jisx2121990': + case 'x212': + return 'JIS_X0212-1990'; + + case 'csiso141jusib1002': + case 'iso646yu': + case 'isoir141': + case 'js': + case 'jusib1002': + case 'yu': + return 'JUS_I.B1.002'; + + case 'csiso147macedonian': + case 'isoir147': + case 'jusib1003mac': + case 'macedonian': + return 'JUS_I.B1.003-mac'; + + case 'csiso146serbian': + case 'isoir146': + case 'jusib1003serb': + case 'serbian': + return 'JUS_I.B1.003-serb'; + + case 'koi7switched': + return 'KOI7-switched'; + + case 'cskoi8r': + case 'koi8r': + return 'KOI8-R'; + + case 'koi8u': + return 'KOI8-U'; + + case 'csksc5636': + case 'iso646kr': + case 'ksc5636': + return 'KSC5636'; + + case 'cskz1048': + case 'kz1048': + case 'rk1048': + case 'strk10482002': + return 'KZ-1048'; + + case 'csiso19latingreek': + case 'isoir19': + case 'latingreek': + return 'latin-greek'; + + case 'csiso27latingreek1': + case 'isoir27': + case 'latingreek1': + return 'Latin-greek-1'; + + case 'csiso158lap': + case 'isoir158': + case 'lap': + case 'latinlap': + return 'latin-lap'; + + case 'csmacintosh': + case 'mac': + case 'macintosh': + return 'macintosh'; + + case 'csmicrosoftpublishing': + case 'microsoftpublishing': + return 'Microsoft-Publishing'; + + case 'csmnem': + case 'mnem': + return 'MNEM'; + + case 'csmnemonic': + case 'mnemonic': + return 'MNEMONIC'; + + case 'csiso86hungarian': + case 'hu': + case 'iso646hu': + case 'isoir86': + case 'msz77953': + return 'MSZ_7795.3'; + + case 'csnatsdano': + case 'isoir91': + case 'natsdano': + return 'NATS-DANO'; + + case 'csnatsdanoadd': + case 'isoir92': + case 'natsdanoadd': + return 'NATS-DANO-ADD'; + + case 'csnatssefi': + case 'isoir81': + case 'natssefi': + return 'NATS-SEFI'; + + case 'csnatssefiadd': + case 'isoir82': + case 'natssefiadd': + return 'NATS-SEFI-ADD'; + + case 'csiso151cuba': + case 'cuba': + case 'iso646cu': + case 'isoir151': + case 'ncnc1081': + return 'NC_NC00-10:81'; + + case 'csiso69french': + case 'fr': + case 'iso646fr': + case 'isoir69': + case 'nfz62010': + return 'NF_Z_62-010'; + + case 'csiso25french': + case 'iso646fr1': + case 'isoir25': + case 'nfz620101973': + return 'NF_Z_62-010_(1973)'; + + case 'csiso60danishnorwegian': + case 'csiso60norwegian1': + case 'iso646no': + case 'isoir60': + case 'no': + case 'ns45511': + return 'NS_4551-1'; + + case 'csiso61norwegian2': + case 'iso646no2': + case 'isoir61': + case 'no2': + case 'ns45512': + return 'NS_4551-2'; + + case 'osdebcdicdf3irv': + return 'OSD_EBCDIC_DF03_IRV'; + + case 'osdebcdicdf41': + return 'OSD_EBCDIC_DF04_1'; + + case 'osdebcdicdf415': + return 'OSD_EBCDIC_DF04_15'; + + case 'cspc8danishnorwegian': + case 'pc8danishnorwegian': + return 'PC8-Danish-Norwegian'; + + case 'cspc8turkish': + case 'pc8turkish': + return 'PC8-Turkish'; + + case 'csiso16portuguese': + case 'iso646pt': + case 'isoir16': + case 'pt': + return 'PT'; + + case 'csiso84portuguese2': + case 'iso646pt2': + case 'isoir84': + case 'pt2': + return 'PT2'; + + case 'cp154': + case 'csptcp154': + case 'cyrillicasian': + case 'pt154': + case 'ptcp154': + return 'PTCP154'; + + case 'scsu': + return 'SCSU'; + + case 'csiso10swedish': + case 'fi': + case 'iso646fi': + case 'iso646se': + case 'isoir10': + case 'se': + case 'sen850200b': + return 'SEN_850200_B'; + + case 'csiso11swedishfornames': + case 'iso646se2': + case 'isoir11': + case 'se2': + case 'sen850200c': + return 'SEN_850200_C'; + + case 'csiso102t617bit': + case 'isoir102': + case 't617bit': + return 'T.61-7bit'; + + case 'csiso103t618bit': + case 'isoir103': + case 't61': + case 't618bit': + return 'T.61-8bit'; + + case 'csiso128t101g2': + case 'isoir128': + case 't101g2': + return 'T.101-G2'; + + case 'cstscii': + case 'tscii': + return 'TSCII'; + + case 'csunicode11': + case 'unicode11': + return 'UNICODE-1-1'; + + case 'csunicode11utf7': + case 'unicode11utf7': + return 'UNICODE-1-1-UTF-7'; + + case 'csunknown8bit': + case 'unknown8bit': + return 'UNKNOWN-8BIT'; + + case 'ansix341968': + case 'ansix341986': + case 'ascii': + case 'cp367': + case 'csascii': + case 'ibm367': + case 'iso646irv1991': + case 'iso646us': + case 'isoir6': + case 'us': + case 'usascii': + return 'US-ASCII'; + + case 'csusdk': + case 'usdk': + return 'us-dk'; + + case 'utf7': + return 'UTF-7'; + + case 'utf8': + return 'UTF-8'; + + case 'utf16': + return 'UTF-16'; + + case 'utf16be': + return 'UTF-16BE'; + + case 'utf16le': + return 'UTF-16LE'; + + case 'utf32': + return 'UTF-32'; + + case 'utf32be': + return 'UTF-32BE'; + + case 'utf32le': + return 'UTF-32LE'; + + case 'csventurainternational': + case 'venturainternational': + return 'Ventura-International'; + + case 'csventuramath': + case 'venturamath': + return 'Ventura-Math'; + + case 'csventuraus': + case 'venturaus': + return 'Ventura-US'; + + case 'csiso70videotexsupp1': + case 'isoir70': + case 'videotexsuppl': + return 'videotex-suppl'; + + case 'csviqr': + case 'viqr': + return 'VIQR'; + + case 'csviscii': + case 'viscii': + return 'VISCII'; + + case 'csshiftjis': + case 'cswindows31j': + case 'mskanji': + case 'shiftjis': + case 'windows31j': + return 'Windows-31J'; + + case 'iso885911': + case 'tis620': + return 'windows-874'; + + case 'cseuckr': + case 'csksc56011987': + case 'euckr': + case 'isoir149': + case 'korean': + case 'ksc5601': + case 'ksc56011987': + case 'ksc56011989': + case 'windows949': + return 'windows-949'; + + case 'windows1250': + return 'windows-1250'; + + case 'windows1251': + return 'windows-1251'; + + case 'cp819': + case 'csisolatin1': + case 'ibm819': + case 'iso88591': + case 'iso885911987': + case 'isoir100': + case 'l1': + case 'latin1': + case 'windows1252': + return 'windows-1252'; + + case 'windows1253': + return 'windows-1253'; + + case 'csisolatin5': + case 'iso88599': + case 'iso885991989': + case 'isoir148': + case 'l5': + case 'latin5': + case 'windows1254': + return 'windows-1254'; + + case 'windows1255': + return 'windows-1255'; + + case 'windows1256': + return 'windows-1256'; + + case 'windows1257': + return 'windows-1257'; + + case 'windows1258': + return 'windows-1258'; + + default: + return $charset; + } + } + + public static function get_curl_version() + { + if (is_array($curl = curl_version())) + { + $curl = $curl['version']; + } + elseif (substr($curl, 0, 5) === 'curl/') + { + $curl = substr($curl, 5, strcspn($curl, "\x09\x0A\x0B\x0C\x0D", 5)); + } + elseif (substr($curl, 0, 8) === 'libcurl/') + { + $curl = substr($curl, 8, strcspn($curl, "\x09\x0A\x0B\x0C\x0D", 8)); + } + else + { + $curl = 0; + } + return $curl; + } + + /** + * Strip HTML comments + * + * @param string $data Data to strip comments from + * @return string Comment stripped string + */ + public static function strip_comments($data) + { + $output = ''; + while (($start = strpos($data, '<!--')) !== false) + { + $output .= substr($data, 0, $start); + if (($end = strpos($data, '-->', $start)) !== false) + { + $data = substr_replace($data, '', 0, $end + 3); + } + else + { + $data = ''; + } + } + return $output . $data; + } + + public static function parse_date($dt) + { + $parser = SimplePie_Parse_Date::get(); + return $parser->parse($dt); + } + + /** + * Decode HTML entities + * + * @deprecated Use DOMDocument instead + * @param string $data Input data + * @return string Output data + */ + public static function entities_decode($data) + { + $decoder = new SimplePie_Decode_HTML_Entities($data); + return $decoder->parse(); + } + + /** + * Remove RFC822 comments + * + * @param string $data Data to strip comments from + * @return string Comment stripped string + */ + public static function uncomment_rfc822($string) + { + $string = (string) $string; + $position = 0; + $length = strlen($string); + $depth = 0; + + $output = ''; + + while ($position < $length && ($pos = strpos($string, '(', $position)) !== false) + { + $output .= substr($string, $position, $pos - $position); + $position = $pos + 1; + if ($string[$pos - 1] !== '\\') + { + $depth++; + while ($depth && $position < $length) + { + $position += strcspn($string, '()', $position); + if ($string[$position - 1] === '\\') + { + $position++; + continue; + } + elseif (isset($string[$position])) + { + switch ($string[$position]) + { + case '(': + $depth++; + break; + + case ')': + $depth--; + break; + } + $position++; + } + else + { + break; + } + } + } + else + { + $output .= '('; + } + } + $output .= substr($string, $position); + + return $output; + } + + public static function parse_mime($mime) + { + if (($pos = strpos($mime, ';')) === false) + { + return trim($mime); + } + else + { + return trim(substr($mime, 0, $pos)); + } + } + + public static function atom_03_construct_type($attribs) + { + if (isset($attribs['']['mode']) && strtolower(trim($attribs['']['mode']) === 'base64')) + { + $mode = SIMPLEPIE_CONSTRUCT_BASE64; + } + else + { + $mode = SIMPLEPIE_CONSTRUCT_NONE; + } + if (isset($attribs['']['type'])) + { + switch (strtolower(trim($attribs['']['type']))) + { + case 'text': + case 'text/plain': + return SIMPLEPIE_CONSTRUCT_TEXT | $mode; + + case 'html': + case 'text/html': + return SIMPLEPIE_CONSTRUCT_HTML | $mode; + + case 'xhtml': + case 'application/xhtml+xml': + return SIMPLEPIE_CONSTRUCT_XHTML | $mode; + + default: + return SIMPLEPIE_CONSTRUCT_NONE | $mode; + } + } + else + { + return SIMPLEPIE_CONSTRUCT_TEXT | $mode; + } + } + + public static function atom_10_construct_type($attribs) + { + if (isset($attribs['']['type'])) + { + switch (strtolower(trim($attribs['']['type']))) + { + case 'text': + return SIMPLEPIE_CONSTRUCT_TEXT; + + case 'html': + return SIMPLEPIE_CONSTRUCT_HTML; + + case 'xhtml': + return SIMPLEPIE_CONSTRUCT_XHTML; + + default: + return SIMPLEPIE_CONSTRUCT_NONE; + } + } + return SIMPLEPIE_CONSTRUCT_TEXT; + } + + public static function atom_10_content_construct_type($attribs) + { + if (isset($attribs['']['type'])) + { + $type = strtolower(trim($attribs['']['type'])); + switch ($type) + { + case 'text': + return SIMPLEPIE_CONSTRUCT_TEXT; + + case 'html': + return SIMPLEPIE_CONSTRUCT_HTML; + + case 'xhtml': + return SIMPLEPIE_CONSTRUCT_XHTML; + } + if (in_array(substr($type, -4), array('+xml', '/xml')) || substr($type, 0, 5) === 'text/') + { + return SIMPLEPIE_CONSTRUCT_NONE; + } + else + { + return SIMPLEPIE_CONSTRUCT_BASE64; + } + } + else + { + return SIMPLEPIE_CONSTRUCT_TEXT; + } + } + + public static function is_isegment_nz_nc($string) + { + return (bool) preg_match('/^([A-Za-z0-9\-._~\x{A0}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFEF}\x{10000}-\x{1FFFD}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}\x{40000}-\x{4FFFD}\x{50000}-\x{5FFFD}\x{60000}-\x{6FFFD}\x{70000}-\x{7FFFD}\x{80000}-\x{8FFFD}\x{90000}-\x{9FFFD}\x{A0000}-\x{AFFFD}\x{B0000}-\x{BFFFD}\x{C0000}-\x{CFFFD}\x{D0000}-\x{DFFFD}\x{E1000}-\x{EFFFD}!$&\'()*+,;=@]|(%[0-9ABCDEF]{2}))+$/u', $string); + } + + public static function space_separated_tokens($string) + { + $space_characters = "\x20\x09\x0A\x0B\x0C\x0D"; + $string_length = strlen($string); + + $position = strspn($string, $space_characters); + $tokens = array(); + + while ($position < $string_length) + { + $len = strcspn($string, $space_characters, $position); + $tokens[] = substr($string, $position, $len); + $position += $len; + $position += strspn($string, $space_characters, $position); + } + + return $tokens; + } + + /** + * Converts a unicode codepoint to a UTF-8 character + * + * @static + * @param int $codepoint Unicode codepoint + * @return string UTF-8 character + */ + public static function codepoint_to_utf8($codepoint) + { + $codepoint = (int) $codepoint; + if ($codepoint < 0) + { + return false; + } + else if ($codepoint <= 0x7f) + { + return chr($codepoint); + } + else if ($codepoint <= 0x7ff) + { + return chr(0xc0 | ($codepoint >> 6)) . chr(0x80 | ($codepoint & 0x3f)); + } + else if ($codepoint <= 0xffff) + { + return chr(0xe0 | ($codepoint >> 12)) . chr(0x80 | (($codepoint >> 6) & 0x3f)) . chr(0x80 | ($codepoint & 0x3f)); + } + else if ($codepoint <= 0x10ffff) + { + return chr(0xf0 | ($codepoint >> 18)) . chr(0x80 | (($codepoint >> 12) & 0x3f)) . chr(0x80 | (($codepoint >> 6) & 0x3f)) . chr(0x80 | ($codepoint & 0x3f)); + } + else + { + // U+FFFD REPLACEMENT CHARACTER + return "\xEF\xBF\xBD"; + } + } + + /** + * Similar to parse_str() + * + * Returns an associative array of name/value pairs, where the value is an + * array of values that have used the same name + * + * @static + * @param string $str The input string. + * @return array + */ + public static function parse_str($str) + { + $return = array(); + $str = explode('&', $str); + + foreach ($str as $section) + { + if (strpos($section, '=') !== false) + { + list($name, $value) = explode('=', $section, 2); + $return[urldecode($name)][] = urldecode($value); + } + else + { + $return[urldecode($section)][] = null; + } + } + + return $return; + } + + /** + * Detect XML encoding, as per XML 1.0 Appendix F.1 + * + * @todo Add support for EBCDIC + * @param string $data XML data + * @param SimplePie_Registry $registry Class registry + * @return array Possible encodings + */ + public static function xml_encoding($data, $registry) + { + // UTF-32 Big Endian BOM + if (substr($data, 0, 4) === "\x00\x00\xFE\xFF") + { + $encoding[] = 'UTF-32BE'; + } + // UTF-32 Little Endian BOM + elseif (substr($data, 0, 4) === "\xFF\xFE\x00\x00") + { + $encoding[] = 'UTF-32LE'; + } + // UTF-16 Big Endian BOM + elseif (substr($data, 0, 2) === "\xFE\xFF") + { + $encoding[] = 'UTF-16BE'; + } + // UTF-16 Little Endian BOM + elseif (substr($data, 0, 2) === "\xFF\xFE") + { + $encoding[] = 'UTF-16LE'; + } + // UTF-8 BOM + elseif (substr($data, 0, 3) === "\xEF\xBB\xBF") + { + $encoding[] = 'UTF-8'; + } + // UTF-32 Big Endian Without BOM + elseif (substr($data, 0, 20) === "\x00\x00\x00\x3C\x00\x00\x00\x3F\x00\x00\x00\x78\x00\x00\x00\x6D\x00\x00\x00\x6C") + { + if ($pos = strpos($data, "\x00\x00\x00\x3F\x00\x00\x00\x3E")) + { + $parser = $registry->create('XML_Declaration_Parser', array(SimplePie_Misc::change_encoding(substr($data, 20, $pos - 20), 'UTF-32BE', 'UTF-8'))); + if ($parser->parse()) + { + $encoding[] = $parser->encoding; + } + } + $encoding[] = 'UTF-32BE'; + } + // UTF-32 Little Endian Without BOM + elseif (substr($data, 0, 20) === "\x3C\x00\x00\x00\x3F\x00\x00\x00\x78\x00\x00\x00\x6D\x00\x00\x00\x6C\x00\x00\x00") + { + if ($pos = strpos($data, "\x3F\x00\x00\x00\x3E\x00\x00\x00")) + { + $parser = $registry->create('XML_Declaration_Parser', array(SimplePie_Misc::change_encoding(substr($data, 20, $pos - 20), 'UTF-32LE', 'UTF-8'))); + if ($parser->parse()) + { + $encoding[] = $parser->encoding; + } + } + $encoding[] = 'UTF-32LE'; + } + // UTF-16 Big Endian Without BOM + elseif (substr($data, 0, 10) === "\x00\x3C\x00\x3F\x00\x78\x00\x6D\x00\x6C") + { + if ($pos = strpos($data, "\x00\x3F\x00\x3E")) + { + $parser = $registry->create('XML_Declaration_Parser', array(SimplePie_Misc::change_encoding(substr($data, 20, $pos - 10), 'UTF-16BE', 'UTF-8'))); + if ($parser->parse()) + { + $encoding[] = $parser->encoding; + } + } + $encoding[] = 'UTF-16BE'; + } + // UTF-16 Little Endian Without BOM + elseif (substr($data, 0, 10) === "\x3C\x00\x3F\x00\x78\x00\x6D\x00\x6C\x00") + { + if ($pos = strpos($data, "\x3F\x00\x3E\x00")) + { + $parser = $registry->create('XML_Declaration_Parser', array(SimplePie_Misc::change_encoding(substr($data, 20, $pos - 10), 'UTF-16LE', 'UTF-8'))); + if ($parser->parse()) + { + $encoding[] = $parser->encoding; + } + } + $encoding[] = 'UTF-16LE'; + } + // US-ASCII (or superset) + elseif (substr($data, 0, 5) === "\x3C\x3F\x78\x6D\x6C") + { + if ($pos = strpos($data, "\x3F\x3E")) + { + $parser = $registry->create('XML_Declaration_Parser', array(substr($data, 5, $pos - 5))); + if ($parser->parse()) + { + $encoding[] = $parser->encoding; + } + } + $encoding[] = 'UTF-8'; + } + // Fallback to UTF-8 + else + { + $encoding[] = 'UTF-8'; + } + return $encoding; + } + + public static function output_javascript() + { + if (function_exists('ob_gzhandler')) + { + ob_start('ob_gzhandler'); + } + header('Content-type: text/javascript; charset: UTF-8'); + header('Cache-Control: must-revalidate'); + header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 604800) . ' GMT'); // 7 days + ?> +function embed_quicktime(type, bgcolor, width, height, link, placeholder, loop) { + if (placeholder != '') { + document.writeln('<embed type="'+type+'" style="cursor:hand; cursor:pointer;" href="'+link+'" src="'+placeholder+'" width="'+width+'" height="'+height+'" autoplay="false" target="myself" controller="false" loop="'+loop+'" scale="aspect" bgcolor="'+bgcolor+'" pluginspage="http://www.apple.com/quicktime/download/"></embed>'); + } + else { + document.writeln('<embed type="'+type+'" style="cursor:hand; cursor:pointer;" src="'+link+'" width="'+width+'" height="'+height+'" autoplay="false" target="myself" controller="true" loop="'+loop+'" scale="aspect" bgcolor="'+bgcolor+'" pluginspage="http://www.apple.com/quicktime/download/"></embed>'); + } +} + +function embed_flash(bgcolor, width, height, link, loop, type) { + document.writeln('<embed src="'+link+'" pluginspage="http://www.macromedia.com/go/getflashplayer" type="'+type+'" quality="high" width="'+width+'" height="'+height+'" bgcolor="'+bgcolor+'" loop="'+loop+'"></embed>'); +} + +function embed_flv(width, height, link, placeholder, loop, player) { + document.writeln('<embed src="'+player+'" pluginspage="http://www.macromedia.com/go/getflashplayer" type="application/x-shockwave-flash" quality="high" width="'+width+'" height="'+height+'" wmode="transparent" flashvars="file='+link+'&autostart=false&repeat='+loop+'&showdigits=true&showfsbutton=false"></embed>'); +} + +function embed_wmedia(width, height, link) { + document.writeln('<embed type="application/x-mplayer2" src="'+link+'" autosize="1" width="'+width+'" height="'+height+'" showcontrols="1" showstatusbar="0" showdisplay="0" autostart="0"></embed>'); +} + <?php + } + + /** + * Get the SimplePie build timestamp + * + * Uses the git index if it exists, otherwise uses the modification time + * of the newest file. + */ + public static function get_build() + { + $root = dirname(dirname(__FILE__)); + if (file_exists($root . '/.git/index')) + { + return filemtime($root . '/.git/index'); + } + elseif (file_exists($root . '/SimplePie')) + { + $time = 0; + foreach (glob($root . '/SimplePie/*.php') as $file) + { + if (($mtime = filemtime($file)) > $time) + { + $time = $mtime; + } + } + return $time; + } + elseif (file_exists(dirname(__FILE__) . '/Core.php')) + { + return filemtime(dirname(__FILE__) . '/Core.php'); + } + else + { + return filemtime(__FILE__); + } + } + + /** + * Format debugging information + */ + public static function debug(&$sp) + { + $info = 'SimplePie ' . SIMPLEPIE_VERSION . ' Build ' . SIMPLEPIE_BUILD . "\n"; + $info .= 'PHP ' . PHP_VERSION . "\n"; + if ($sp->error() !== null) + { + $info .= 'Error occurred: ' . $sp->error() . "\n"; + } + else + { + $info .= "No error found.\n"; + } + $info .= "Extensions:\n"; + $extensions = array('pcre', 'curl', 'zlib', 'mbstring', 'iconv', 'xmlreader', 'xml'); + foreach ($extensions as $ext) + { + if (extension_loaded($ext)) + { + $info .= " $ext loaded\n"; + switch ($ext) + { + case 'pcre': + $info .= ' Version ' . PCRE_VERSION . "\n"; + break; + case 'curl': + $version = curl_version(); + $info .= ' Version ' . $version['version'] . "\n"; + break; + case 'mbstring': + $info .= ' Overloading: ' . mb_get_info('func_overload') . "\n"; + break; + case 'iconv': + $info .= ' Version ' . ICONV_VERSION . "\n"; + break; + case 'xml': + $info .= ' Version ' . LIBXML_DOTTED_VERSION . "\n"; + break; + } + } + else + { + $info .= " $ext not loaded\n"; + } + } + return $info; + } + + public static function silence_errors($num, $str) + { + // No-op + } +} + diff --git a/vendor/simplepie/simplepie/library/SimplePie/Net/IPv6.php b/vendor/simplepie/simplepie/library/SimplePie/Net/IPv6.php new file mode 100644 index 000000000..47658aff2 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Net/IPv6.php @@ -0,0 +1,275 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + + +/** + * Class to validate and to work with IPv6 addresses. + * + * @package SimplePie + * @subpackage HTTP + * @copyright 2003-2005 The PHP Group + * @license http://www.opensource.org/licenses/bsd-license.php + * @link http://pear.php.net/package/Net_IPv6 + * @author Alexander Merz <alexander.merz@web.de> + * @author elfrink at introweb dot nl + * @author Josh Peck <jmp at joshpeck dot org> + * @author Geoffrey Sneddon <geoffers@gmail.com> + */ +class SimplePie_Net_IPv6 +{ + /** + * Uncompresses an IPv6 address + * + * RFC 4291 allows you to compress concecutive zero pieces in an address to + * '::'. This method expects a valid IPv6 address and expands the '::' to + * the required number of zero pieces. + * + * Example: FF01::101 -> FF01:0:0:0:0:0:0:101 + * ::1 -> 0:0:0:0:0:0:0:1 + * + * @author Alexander Merz <alexander.merz@web.de> + * @author elfrink at introweb dot nl + * @author Josh Peck <jmp at joshpeck dot org> + * @copyright 2003-2005 The PHP Group + * @license http://www.opensource.org/licenses/bsd-license.php + * @param string $ip An IPv6 address + * @return string The uncompressed IPv6 address + */ + public static function uncompress($ip) + { + $c1 = -1; + $c2 = -1; + if (substr_count($ip, '::') === 1) + { + list($ip1, $ip2) = explode('::', $ip); + if ($ip1 === '') + { + $c1 = -1; + } + else + { + $c1 = substr_count($ip1, ':'); + } + if ($ip2 === '') + { + $c2 = -1; + } + else + { + $c2 = substr_count($ip2, ':'); + } + if (strpos($ip2, '.') !== false) + { + $c2++; + } + // :: + if ($c1 === -1 && $c2 === -1) + { + $ip = '0:0:0:0:0:0:0:0'; + } + // ::xxx + else if ($c1 === -1) + { + $fill = str_repeat('0:', 7 - $c2); + $ip = str_replace('::', $fill, $ip); + } + // xxx:: + else if ($c2 === -1) + { + $fill = str_repeat(':0', 7 - $c1); + $ip = str_replace('::', $fill, $ip); + } + // xxx::xxx + else + { + $fill = ':' . str_repeat('0:', 6 - $c2 - $c1); + $ip = str_replace('::', $fill, $ip); + } + } + return $ip; + } + + /** + * Compresses an IPv6 address + * + * RFC 4291 allows you to compress concecutive zero pieces in an address to + * '::'. This method expects a valid IPv6 address and compresses consecutive + * zero pieces to '::'. + * + * Example: FF01:0:0:0:0:0:0:101 -> FF01::101 + * 0:0:0:0:0:0:0:1 -> ::1 + * + * @see uncompress() + * @param string $ip An IPv6 address + * @return string The compressed IPv6 address + */ + public static function compress($ip) + { + // Prepare the IP to be compressed + $ip = self::uncompress($ip); + $ip_parts = self::split_v6_v4($ip); + + // Replace all leading zeros + $ip_parts[0] = preg_replace('/(^|:)0+([0-9])/', '\1\2', $ip_parts[0]); + + // Find bunches of zeros + if (preg_match_all('/(?:^|:)(?:0(?::|$))+/', $ip_parts[0], $matches, PREG_OFFSET_CAPTURE)) + { + $max = 0; + $pos = null; + foreach ($matches[0] as $match) + { + if (strlen($match[0]) > $max) + { + $max = strlen($match[0]); + $pos = $match[1]; + } + } + + $ip_parts[0] = substr_replace($ip_parts[0], '::', $pos, $max); + } + + if ($ip_parts[1] !== '') + { + return implode(':', $ip_parts); + } + else + { + return $ip_parts[0]; + } + } + + /** + * Splits an IPv6 address into the IPv6 and IPv4 representation parts + * + * RFC 4291 allows you to represent the last two parts of an IPv6 address + * using the standard IPv4 representation + * + * Example: 0:0:0:0:0:0:13.1.68.3 + * 0:0:0:0:0:FFFF:129.144.52.38 + * + * @param string $ip An IPv6 address + * @return array [0] contains the IPv6 represented part, and [1] the IPv4 represented part + */ + private static function split_v6_v4($ip) + { + if (strpos($ip, '.') !== false) + { + $pos = strrpos($ip, ':'); + $ipv6_part = substr($ip, 0, $pos); + $ipv4_part = substr($ip, $pos + 1); + return array($ipv6_part, $ipv4_part); + } + else + { + return array($ip, ''); + } + } + + /** + * Checks an IPv6 address + * + * Checks if the given IP is a valid IPv6 address + * + * @param string $ip An IPv6 address + * @return bool true if $ip is a valid IPv6 address + */ + public static function check_ipv6($ip) + { + $ip = self::uncompress($ip); + list($ipv6, $ipv4) = self::split_v6_v4($ip); + $ipv6 = explode(':', $ipv6); + $ipv4 = explode('.', $ipv4); + if (count($ipv6) === 8 && count($ipv4) === 1 || count($ipv6) === 6 && count($ipv4) === 4) + { + foreach ($ipv6 as $ipv6_part) + { + // The section can't be empty + if ($ipv6_part === '') + return false; + + // Nor can it be over four characters + if (strlen($ipv6_part) > 4) + return false; + + // Remove leading zeros (this is safe because of the above) + $ipv6_part = ltrim($ipv6_part, '0'); + if ($ipv6_part === '') + $ipv6_part = '0'; + + // Check the value is valid + $value = hexdec($ipv6_part); + if (dechex($value) !== strtolower($ipv6_part) || $value < 0 || $value > 0xFFFF) + return false; + } + if (count($ipv4) === 4) + { + foreach ($ipv4 as $ipv4_part) + { + $value = (int) $ipv4_part; + if ((string) $value !== $ipv4_part || $value < 0 || $value > 0xFF) + return false; + } + } + return true; + } + else + { + return false; + } + } + + /** + * Checks if the given IP is a valid IPv6 address + * + * @codeCoverageIgnore + * @deprecated Use {@see SimplePie_Net_IPv6::check_ipv6()} instead + * @see check_ipv6 + * @param string $ip An IPv6 address + * @return bool true if $ip is a valid IPv6 address + */ + public static function checkIPv6($ip) + { + return self::check_ipv6($ip); + } +} diff --git a/vendor/simplepie/simplepie/library/SimplePie/Parse/Date.php b/vendor/simplepie/simplepie/library/SimplePie/Parse/Date.php new file mode 100644 index 000000000..1f2156655 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Parse/Date.php @@ -0,0 +1,983 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + + +/** + * Date Parser + * + * @package SimplePie + * @subpackage Parsing + */ +class SimplePie_Parse_Date +{ + /** + * Input data + * + * @access protected + * @var string + */ + var $date; + + /** + * List of days, calendar day name => ordinal day number in the week + * + * @access protected + * @var array + */ + var $day = array( + // English + 'mon' => 1, + 'monday' => 1, + 'tue' => 2, + 'tuesday' => 2, + 'wed' => 3, + 'wednesday' => 3, + 'thu' => 4, + 'thursday' => 4, + 'fri' => 5, + 'friday' => 5, + 'sat' => 6, + 'saturday' => 6, + 'sun' => 7, + 'sunday' => 7, + // Dutch + 'maandag' => 1, + 'dinsdag' => 2, + 'woensdag' => 3, + 'donderdag' => 4, + 'vrijdag' => 5, + 'zaterdag' => 6, + 'zondag' => 7, + // French + 'lundi' => 1, + 'mardi' => 2, + 'mercredi' => 3, + 'jeudi' => 4, + 'vendredi' => 5, + 'samedi' => 6, + 'dimanche' => 7, + // German + 'montag' => 1, + 'dienstag' => 2, + 'mittwoch' => 3, + 'donnerstag' => 4, + 'freitag' => 5, + 'samstag' => 6, + 'sonnabend' => 6, + 'sonntag' => 7, + // Italian + 'lunedì' => 1, + 'martedì' => 2, + 'mercoledì' => 3, + 'giovedì' => 4, + 'venerdì' => 5, + 'sabato' => 6, + 'domenica' => 7, + // Spanish + 'lunes' => 1, + 'martes' => 2, + 'miércoles' => 3, + 'jueves' => 4, + 'viernes' => 5, + 'sábado' => 6, + 'domingo' => 7, + // Finnish + 'maanantai' => 1, + 'tiistai' => 2, + 'keskiviikko' => 3, + 'torstai' => 4, + 'perjantai' => 5, + 'lauantai' => 6, + 'sunnuntai' => 7, + // Hungarian + 'hétfő' => 1, + 'kedd' => 2, + 'szerda' => 3, + 'csütörtok' => 4, + 'péntek' => 5, + 'szombat' => 6, + 'vasárnap' => 7, + // Greek + 'Δευ' => 1, + 'Τρι' => 2, + 'Τετ' => 3, + 'Πεμ' => 4, + 'Παρ' => 5, + 'Σαβ' => 6, + 'Κυρ' => 7, + ); + + /** + * List of months, calendar month name => calendar month number + * + * @access protected + * @var array + */ + var $month = array( + // English + 'jan' => 1, + 'january' => 1, + 'feb' => 2, + 'february' => 2, + 'mar' => 3, + 'march' => 3, + 'apr' => 4, + 'april' => 4, + 'may' => 5, + // No long form of May + 'jun' => 6, + 'june' => 6, + 'jul' => 7, + 'july' => 7, + 'aug' => 8, + 'august' => 8, + 'sep' => 9, + 'september' => 9, + 'oct' => 10, + 'october' => 10, + 'nov' => 11, + 'november' => 11, + 'dec' => 12, + 'december' => 12, + // Dutch + 'januari' => 1, + 'februari' => 2, + 'maart' => 3, + 'april' => 4, + 'mei' => 5, + 'juni' => 6, + 'juli' => 7, + 'augustus' => 8, + 'september' => 9, + 'oktober' => 10, + 'november' => 11, + 'december' => 12, + // French + 'janvier' => 1, + 'février' => 2, + 'mars' => 3, + 'avril' => 4, + 'mai' => 5, + 'juin' => 6, + 'juillet' => 7, + 'août' => 8, + 'septembre' => 9, + 'octobre' => 10, + 'novembre' => 11, + 'décembre' => 12, + // German + 'januar' => 1, + 'februar' => 2, + 'märz' => 3, + 'april' => 4, + 'mai' => 5, + 'juni' => 6, + 'juli' => 7, + 'august' => 8, + 'september' => 9, + 'oktober' => 10, + 'november' => 11, + 'dezember' => 12, + // Italian + 'gennaio' => 1, + 'febbraio' => 2, + 'marzo' => 3, + 'aprile' => 4, + 'maggio' => 5, + 'giugno' => 6, + 'luglio' => 7, + 'agosto' => 8, + 'settembre' => 9, + 'ottobre' => 10, + 'novembre' => 11, + 'dicembre' => 12, + // Spanish + 'enero' => 1, + 'febrero' => 2, + 'marzo' => 3, + 'abril' => 4, + 'mayo' => 5, + 'junio' => 6, + 'julio' => 7, + 'agosto' => 8, + 'septiembre' => 9, + 'setiembre' => 9, + 'octubre' => 10, + 'noviembre' => 11, + 'diciembre' => 12, + // Finnish + 'tammikuu' => 1, + 'helmikuu' => 2, + 'maaliskuu' => 3, + 'huhtikuu' => 4, + 'toukokuu' => 5, + 'kesäkuu' => 6, + 'heinäkuu' => 7, + 'elokuu' => 8, + 'suuskuu' => 9, + 'lokakuu' => 10, + 'marras' => 11, + 'joulukuu' => 12, + // Hungarian + 'január' => 1, + 'február' => 2, + 'március' => 3, + 'április' => 4, + 'május' => 5, + 'június' => 6, + 'július' => 7, + 'augusztus' => 8, + 'szeptember' => 9, + 'október' => 10, + 'november' => 11, + 'december' => 12, + // Greek + 'Ιαν' => 1, + 'Φεβ' => 2, + 'Μάώ' => 3, + 'Μαώ' => 3, + 'Απρ' => 4, + 'Μάι' => 5, + 'Μαϊ' => 5, + 'Μαι' => 5, + 'Ιούν' => 6, + 'Ιον' => 6, + 'Ιούλ' => 7, + 'Ιολ' => 7, + 'Αύγ' => 8, + 'Αυγ' => 8, + 'Σεπ' => 9, + 'Οκτ' => 10, + 'Νοέ' => 11, + 'Δεκ' => 12, + ); + + /** + * List of timezones, abbreviation => offset from UTC + * + * @access protected + * @var array + */ + var $timezone = array( + 'ACDT' => 37800, + 'ACIT' => 28800, + 'ACST' => 34200, + 'ACT' => -18000, + 'ACWDT' => 35100, + 'ACWST' => 31500, + 'AEDT' => 39600, + 'AEST' => 36000, + 'AFT' => 16200, + 'AKDT' => -28800, + 'AKST' => -32400, + 'AMDT' => 18000, + 'AMT' => -14400, + 'ANAST' => 46800, + 'ANAT' => 43200, + 'ART' => -10800, + 'AZOST' => -3600, + 'AZST' => 18000, + 'AZT' => 14400, + 'BIOT' => 21600, + 'BIT' => -43200, + 'BOT' => -14400, + 'BRST' => -7200, + 'BRT' => -10800, + 'BST' => 3600, + 'BTT' => 21600, + 'CAST' => 18000, + 'CAT' => 7200, + 'CCT' => 23400, + 'CDT' => -18000, + 'CEDT' => 7200, + 'CEST' => 7200, + 'CET' => 3600, + 'CGST' => -7200, + 'CGT' => -10800, + 'CHADT' => 49500, + 'CHAST' => 45900, + 'CIST' => -28800, + 'CKT' => -36000, + 'CLDT' => -10800, + 'CLST' => -14400, + 'COT' => -18000, + 'CST' => -21600, + 'CVT' => -3600, + 'CXT' => 25200, + 'DAVT' => 25200, + 'DTAT' => 36000, + 'EADT' => -18000, + 'EAST' => -21600, + 'EAT' => 10800, + 'ECT' => -18000, + 'EDT' => -14400, + 'EEST' => 10800, + 'EET' => 7200, + 'EGT' => -3600, + 'EKST' => 21600, + 'EST' => -18000, + 'FJT' => 43200, + 'FKDT' => -10800, + 'FKST' => -14400, + 'FNT' => -7200, + 'GALT' => -21600, + 'GEDT' => 14400, + 'GEST' => 10800, + 'GFT' => -10800, + 'GILT' => 43200, + 'GIT' => -32400, + 'GST' => 14400, + 'GST' => -7200, + 'GYT' => -14400, + 'HAA' => -10800, + 'HAC' => -18000, + 'HADT' => -32400, + 'HAE' => -14400, + 'HAP' => -25200, + 'HAR' => -21600, + 'HAST' => -36000, + 'HAT' => -9000, + 'HAY' => -28800, + 'HKST' => 28800, + 'HMT' => 18000, + 'HNA' => -14400, + 'HNC' => -21600, + 'HNE' => -18000, + 'HNP' => -28800, + 'HNR' => -25200, + 'HNT' => -12600, + 'HNY' => -32400, + 'IRDT' => 16200, + 'IRKST' => 32400, + 'IRKT' => 28800, + 'IRST' => 12600, + 'JFDT' => -10800, + 'JFST' => -14400, + 'JST' => 32400, + 'KGST' => 21600, + 'KGT' => 18000, + 'KOST' => 39600, + 'KOVST' => 28800, + 'KOVT' => 25200, + 'KRAST' => 28800, + 'KRAT' => 25200, + 'KST' => 32400, + 'LHDT' => 39600, + 'LHST' => 37800, + 'LINT' => 50400, + 'LKT' => 21600, + 'MAGST' => 43200, + 'MAGT' => 39600, + 'MAWT' => 21600, + 'MDT' => -21600, + 'MESZ' => 7200, + 'MEZ' => 3600, + 'MHT' => 43200, + 'MIT' => -34200, + 'MNST' => 32400, + 'MSDT' => 14400, + 'MSST' => 10800, + 'MST' => -25200, + 'MUT' => 14400, + 'MVT' => 18000, + 'MYT' => 28800, + 'NCT' => 39600, + 'NDT' => -9000, + 'NFT' => 41400, + 'NMIT' => 36000, + 'NOVST' => 25200, + 'NOVT' => 21600, + 'NPT' => 20700, + 'NRT' => 43200, + 'NST' => -12600, + 'NUT' => -39600, + 'NZDT' => 46800, + 'NZST' => 43200, + 'OMSST' => 25200, + 'OMST' => 21600, + 'PDT' => -25200, + 'PET' => -18000, + 'PETST' => 46800, + 'PETT' => 43200, + 'PGT' => 36000, + 'PHOT' => 46800, + 'PHT' => 28800, + 'PKT' => 18000, + 'PMDT' => -7200, + 'PMST' => -10800, + 'PONT' => 39600, + 'PST' => -28800, + 'PWT' => 32400, + 'PYST' => -10800, + 'PYT' => -14400, + 'RET' => 14400, + 'ROTT' => -10800, + 'SAMST' => 18000, + 'SAMT' => 14400, + 'SAST' => 7200, + 'SBT' => 39600, + 'SCDT' => 46800, + 'SCST' => 43200, + 'SCT' => 14400, + 'SEST' => 3600, + 'SGT' => 28800, + 'SIT' => 28800, + 'SRT' => -10800, + 'SST' => -39600, + 'SYST' => 10800, + 'SYT' => 7200, + 'TFT' => 18000, + 'THAT' => -36000, + 'TJT' => 18000, + 'TKT' => -36000, + 'TMT' => 18000, + 'TOT' => 46800, + 'TPT' => 32400, + 'TRUT' => 36000, + 'TVT' => 43200, + 'TWT' => 28800, + 'UYST' => -7200, + 'UYT' => -10800, + 'UZT' => 18000, + 'VET' => -14400, + 'VLAST' => 39600, + 'VLAT' => 36000, + 'VOST' => 21600, + 'VUT' => 39600, + 'WAST' => 7200, + 'WAT' => 3600, + 'WDT' => 32400, + 'WEST' => 3600, + 'WFT' => 43200, + 'WIB' => 25200, + 'WIT' => 32400, + 'WITA' => 28800, + 'WKST' => 18000, + 'WST' => 28800, + 'YAKST' => 36000, + 'YAKT' => 32400, + 'YAPT' => 36000, + 'YEKST' => 21600, + 'YEKT' => 18000, + ); + + /** + * Cached PCRE for SimplePie_Parse_Date::$day + * + * @access protected + * @var string + */ + var $day_pcre; + + /** + * Cached PCRE for SimplePie_Parse_Date::$month + * + * @access protected + * @var string + */ + var $month_pcre; + + /** + * Array of user-added callback methods + * + * @access private + * @var array + */ + var $built_in = array(); + + /** + * Array of user-added callback methods + * + * @access private + * @var array + */ + var $user = array(); + + /** + * Create new SimplePie_Parse_Date object, and set self::day_pcre, + * self::month_pcre, and self::built_in + * + * @access private + */ + public function __construct() + { + $this->day_pcre = '(' . implode(array_keys($this->day), '|') . ')'; + $this->month_pcre = '(' . implode(array_keys($this->month), '|') . ')'; + + static $cache; + if (!isset($cache[get_class($this)])) + { + $all_methods = get_class_methods($this); + + foreach ($all_methods as $method) + { + if (strtolower(substr($method, 0, 5)) === 'date_') + { + $cache[get_class($this)][] = $method; + } + } + } + + foreach ($cache[get_class($this)] as $method) + { + $this->built_in[] = $method; + } + } + + /** + * Get the object + * + * @access public + */ + public static function get() + { + static $object; + if (!$object) + { + $object = new SimplePie_Parse_Date; + } + return $object; + } + + /** + * Parse a date + * + * @final + * @access public + * @param string $date Date to parse + * @return int Timestamp corresponding to date string, or false on failure + */ + public function parse($date) + { + foreach ($this->user as $method) + { + if (($returned = call_user_func($method, $date)) !== false) + { + return $returned; + } + } + + foreach ($this->built_in as $method) + { + if (($returned = call_user_func(array($this, $method), $date)) !== false) + { + return $returned; + } + } + + return false; + } + + /** + * Add a callback method to parse a date + * + * @final + * @access public + * @param callback $callback + */ + public function add_callback($callback) + { + if (is_callable($callback)) + { + $this->user[] = $callback; + } + else + { + trigger_error('User-supplied function must be a valid callback', E_USER_WARNING); + } + } + + /** + * Parse a superset of W3C-DTF (allows hyphens and colons to be omitted, as + * well as allowing any of upper or lower case "T", horizontal tabs, or + * spaces to be used as the time separator (including more than one)) + * + * @access protected + * @return int Timestamp + */ + public function date_w3cdtf($date) + { + static $pcre; + if (!$pcre) + { + $year = '([0-9]{4})'; + $month = $day = $hour = $minute = $second = '([0-9]{2})'; + $decimal = '([0-9]*)'; + $zone = '(?:(Z)|([+\-])([0-9]{1,2}):?([0-9]{1,2}))'; + $pcre = '/^' . $year . '(?:-?' . $month . '(?:-?' . $day . '(?:[Tt\x09\x20]+' . $hour . '(?::?' . $minute . '(?::?' . $second . '(?:.' . $decimal . ')?)?)?' . $zone . ')?)?)?$/'; + } + if (preg_match($pcre, $date, $match)) + { + /* + Capturing subpatterns: + 1: Year + 2: Month + 3: Day + 4: Hour + 5: Minute + 6: Second + 7: Decimal fraction of a second + 8: Zulu + 9: Timezone ± + 10: Timezone hours + 11: Timezone minutes + */ + + // Fill in empty matches + for ($i = count($match); $i <= 3; $i++) + { + $match[$i] = '1'; + } + + for ($i = count($match); $i <= 7; $i++) + { + $match[$i] = '0'; + } + + // Numeric timezone + if (isset($match[9]) && $match[9] !== '') + { + $timezone = $match[10] * 3600; + $timezone += $match[11] * 60; + if ($match[9] === '-') + { + $timezone = 0 - $timezone; + } + } + else + { + $timezone = 0; + } + + // Convert the number of seconds to an integer, taking decimals into account + $second = round((int)$match[6] + (int)$match[7] / pow(10, strlen($match[7]))); + + return gmmktime($match[4], $match[5], $second, $match[2], $match[3], $match[1]) - $timezone; + } + else + { + return false; + } + } + + /** + * Remove RFC822 comments + * + * @access protected + * @param string $data Data to strip comments from + * @return string Comment stripped string + */ + public function remove_rfc2822_comments($string) + { + $string = (string) $string; + $position = 0; + $length = strlen($string); + $depth = 0; + + $output = ''; + + while ($position < $length && ($pos = strpos($string, '(', $position)) !== false) + { + $output .= substr($string, $position, $pos - $position); + $position = $pos + 1; + if ($pos === 0 || $string[$pos - 1] !== '\\') + { + $depth++; + while ($depth && $position < $length) + { + $position += strcspn($string, '()', $position); + if ($string[$position - 1] === '\\') + { + $position++; + continue; + } + elseif (isset($string[$position])) + { + switch ($string[$position]) + { + case '(': + $depth++; + break; + + case ')': + $depth--; + break; + } + $position++; + } + else + { + break; + } + } + } + else + { + $output .= '('; + } + } + $output .= substr($string, $position); + + return $output; + } + + /** + * Parse RFC2822's date format + * + * @access protected + * @return int Timestamp + */ + public function date_rfc2822($date) + { + static $pcre; + if (!$pcre) + { + $wsp = '[\x09\x20]'; + $fws = '(?:' . $wsp . '+|' . $wsp . '*(?:\x0D\x0A' . $wsp . '+)+)'; + $optional_fws = $fws . '?'; + $day_name = $this->day_pcre; + $month = $this->month_pcre; + $day = '([0-9]{1,2})'; + $hour = $minute = $second = '([0-9]{2})'; + $year = '([0-9]{2,4})'; + $num_zone = '([+\-])([0-9]{2})([0-9]{2})'; + $character_zone = '([A-Z]{1,5})'; + $zone = '(?:' . $num_zone . '|' . $character_zone . ')'; + $pcre = '/(?:' . $optional_fws . $day_name . $optional_fws . ',)?' . $optional_fws . $day . $fws . $month . $fws . $year . $fws . $hour . $optional_fws . ':' . $optional_fws . $minute . '(?:' . $optional_fws . ':' . $optional_fws . $second . ')?' . $fws . $zone . '/i'; + } + if (preg_match($pcre, $this->remove_rfc2822_comments($date), $match)) + { + /* + Capturing subpatterns: + 1: Day name + 2: Day + 3: Month + 4: Year + 5: Hour + 6: Minute + 7: Second + 8: Timezone ± + 9: Timezone hours + 10: Timezone minutes + 11: Alphabetic timezone + */ + + // Find the month number + $month = $this->month[strtolower($match[3])]; + + // Numeric timezone + if ($match[8] !== '') + { + $timezone = $match[9] * 3600; + $timezone += $match[10] * 60; + if ($match[8] === '-') + { + $timezone = 0 - $timezone; + } + } + // Character timezone + elseif (isset($this->timezone[strtoupper($match[11])])) + { + $timezone = $this->timezone[strtoupper($match[11])]; + } + // Assume everything else to be -0000 + else + { + $timezone = 0; + } + + // Deal with 2/3 digit years + if ($match[4] < 50) + { + $match[4] += 2000; + } + elseif ($match[4] < 1000) + { + $match[4] += 1900; + } + + // Second is optional, if it is empty set it to zero + if ($match[7] !== '') + { + $second = $match[7]; + } + else + { + $second = 0; + } + + return gmmktime($match[5], $match[6], $second, $month, $match[2], $match[4]) - $timezone; + } + else + { + return false; + } + } + + /** + * Parse RFC850's date format + * + * @access protected + * @return int Timestamp + */ + public function date_rfc850($date) + { + static $pcre; + if (!$pcre) + { + $space = '[\x09\x20]+'; + $day_name = $this->day_pcre; + $month = $this->month_pcre; + $day = '([0-9]{1,2})'; + $year = $hour = $minute = $second = '([0-9]{2})'; + $zone = '([A-Z]{1,5})'; + $pcre = '/^' . $day_name . ',' . $space . $day . '-' . $month . '-' . $year . $space . $hour . ':' . $minute . ':' . $second . $space . $zone . '$/i'; + } + if (preg_match($pcre, $date, $match)) + { + /* + Capturing subpatterns: + 1: Day name + 2: Day + 3: Month + 4: Year + 5: Hour + 6: Minute + 7: Second + 8: Timezone + */ + + // Month + $month = $this->month[strtolower($match[3])]; + + // Character timezone + if (isset($this->timezone[strtoupper($match[8])])) + { + $timezone = $this->timezone[strtoupper($match[8])]; + } + // Assume everything else to be -0000 + else + { + $timezone = 0; + } + + // Deal with 2 digit year + if ($match[4] < 50) + { + $match[4] += 2000; + } + else + { + $match[4] += 1900; + } + + return gmmktime($match[5], $match[6], $match[7], $month, $match[2], $match[4]) - $timezone; + } + else + { + return false; + } + } + + /** + * Parse C99's asctime()'s date format + * + * @access protected + * @return int Timestamp + */ + public function date_asctime($date) + { + static $pcre; + if (!$pcre) + { + $space = '[\x09\x20]+'; + $wday_name = $this->day_pcre; + $mon_name = $this->month_pcre; + $day = '([0-9]{1,2})'; + $hour = $sec = $min = '([0-9]{2})'; + $year = '([0-9]{4})'; + $terminator = '\x0A?\x00?'; + $pcre = '/^' . $wday_name . $space . $mon_name . $space . $day . $space . $hour . ':' . $min . ':' . $sec . $space . $year . $terminator . '$/i'; + } + if (preg_match($pcre, $date, $match)) + { + /* + Capturing subpatterns: + 1: Day name + 2: Month + 3: Day + 4: Hour + 5: Minute + 6: Second + 7: Year + */ + + $month = $this->month[strtolower($match[2])]; + return gmmktime($match[4], $match[5], $match[6], $month, $match[3], $match[7]); + } + else + { + return false; + } + } + + /** + * Parse dates using strtotime() + * + * @access protected + * @return int Timestamp + */ + public function date_strtotime($date) + { + $strtotime = strtotime($date); + if ($strtotime === -1 || $strtotime === false) + { + return false; + } + else + { + return $strtotime; + } + } +} + diff --git a/vendor/simplepie/simplepie/library/SimplePie/Parser.php b/vendor/simplepie/simplepie/library/SimplePie/Parser.php new file mode 100644 index 000000000..17139abe9 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Parser.php @@ -0,0 +1,656 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Parses XML into something sane + * + * + * This class can be overloaded with {@see SimplePie::set_parser_class()} + * + * @package SimplePie + * @subpackage Parsing + */ +class SimplePie_Parser +{ + var $error_code; + var $error_string; + var $current_line; + var $current_column; + var $current_byte; + var $separator = ' '; + var $namespace = array(''); + var $element = array(''); + var $xml_base = array(''); + var $xml_base_explicit = array(false); + var $xml_lang = array(''); + var $data = array(); + var $datas = array(array()); + var $current_xhtml_construct = -1; + var $encoding; + protected $registry; + + public function set_registry(SimplePie_Registry $registry) + { + $this->registry = $registry; + } + + public function parse(&$data, $encoding, $url = '') + { + if (function_exists('Mf2\parse')) { + // Check for both h-feed and h-entry, as both a feed with no entries + // and a list of entries without an h-feed wrapper are both valid. + $position = 0; + while ($position = strpos($data, 'h-feed', $position)) { + $start = $position < 200 ? 0 : $position - 200; + $check = substr($data, $start, 400); + if (preg_match('/class="[^"]*h-feed/', $check)) { + return $this->parse_microformats($data, $url); + } + $position += 7; + } + $position = 0; + while ($position = strpos($data, 'h-entry', $position)) { + $start = $position < 200 ? 0 : $position - 200; + $check = substr($data, $start, 400); + if (preg_match('/class="[^"]*h-entry/', $check)) { + return $this->parse_microformats($data, $url); + } + $position += 7; + } + } + + // Use UTF-8 if we get passed US-ASCII, as every US-ASCII character is a UTF-8 character + if (strtoupper($encoding) === 'US-ASCII') + { + $this->encoding = 'UTF-8'; + } + else + { + $this->encoding = $encoding; + } + + // Strip BOM: + // UTF-32 Big Endian BOM + if (substr($data, 0, 4) === "\x00\x00\xFE\xFF") + { + $data = substr($data, 4); + } + // UTF-32 Little Endian BOM + elseif (substr($data, 0, 4) === "\xFF\xFE\x00\x00") + { + $data = substr($data, 4); + } + // UTF-16 Big Endian BOM + elseif (substr($data, 0, 2) === "\xFE\xFF") + { + $data = substr($data, 2); + } + // UTF-16 Little Endian BOM + elseif (substr($data, 0, 2) === "\xFF\xFE") + { + $data = substr($data, 2); + } + // UTF-8 BOM + elseif (substr($data, 0, 3) === "\xEF\xBB\xBF") + { + $data = substr($data, 3); + } + + if (substr($data, 0, 5) === '<?xml' && strspn(substr($data, 5, 1), "\x09\x0A\x0D\x20") && ($pos = strpos($data, '?>')) !== false) + { + $declaration = $this->registry->create('XML_Declaration_Parser', array(substr($data, 5, $pos - 5))); + if ($declaration->parse()) + { + $data = substr($data, $pos + 2); + $data = '<?xml version="' . $declaration->version . '" encoding="' . $encoding . '" standalone="' . (($declaration->standalone) ? 'yes' : 'no') . '"?>' ."\n". $this->declare_html_entities() . $data; + } + else + { + $this->error_string = 'SimplePie bug! Please report this!'; + return false; + } + } + + $return = true; + + static $xml_is_sane = null; + if ($xml_is_sane === null) + { + $parser_check = xml_parser_create(); + xml_parse_into_struct($parser_check, '<foo>&</foo>', $values); + xml_parser_free($parser_check); + $xml_is_sane = isset($values[0]['value']); + } + + // Create the parser + if ($xml_is_sane) + { + $xml = xml_parser_create_ns($this->encoding, $this->separator); + xml_parser_set_option($xml, XML_OPTION_SKIP_WHITE, 1); + xml_parser_set_option($xml, XML_OPTION_CASE_FOLDING, 0); + xml_set_object($xml, $this); + xml_set_character_data_handler($xml, 'cdata'); + xml_set_element_handler($xml, 'tag_open', 'tag_close'); + + // Parse! + if (!xml_parse($xml, $data, true)) + { + $this->error_code = xml_get_error_code($xml); + $this->error_string = xml_error_string($this->error_code); + $return = false; + } + $this->current_line = xml_get_current_line_number($xml); + $this->current_column = xml_get_current_column_number($xml); + $this->current_byte = xml_get_current_byte_index($xml); + xml_parser_free($xml); + return $return; + } + else + { + libxml_clear_errors(); + $xml = new XMLReader(); + $xml->xml($data); + while (@$xml->read()) + { + switch ($xml->nodeType) + { + + case constant('XMLReader::END_ELEMENT'): + if ($xml->namespaceURI !== '') + { + $tagName = $xml->namespaceURI . $this->separator . $xml->localName; + } + else + { + $tagName = $xml->localName; + } + $this->tag_close(null, $tagName); + break; + case constant('XMLReader::ELEMENT'): + $empty = $xml->isEmptyElement; + if ($xml->namespaceURI !== '') + { + $tagName = $xml->namespaceURI . $this->separator . $xml->localName; + } + else + { + $tagName = $xml->localName; + } + $attributes = array(); + while ($xml->moveToNextAttribute()) + { + if ($xml->namespaceURI !== '') + { + $attrName = $xml->namespaceURI . $this->separator . $xml->localName; + } + else + { + $attrName = $xml->localName; + } + $attributes[$attrName] = $xml->value; + } + $this->tag_open(null, $tagName, $attributes); + if ($empty) + { + $this->tag_close(null, $tagName); + } + break; + case constant('XMLReader::TEXT'): + + case constant('XMLReader::CDATA'): + $this->cdata(null, $xml->value); + break; + } + } + if ($error = libxml_get_last_error()) + { + $this->error_code = $error->code; + $this->error_string = $error->message; + $this->current_line = $error->line; + $this->current_column = $error->column; + return false; + } + else + { + return true; + } + } + } + + public function get_error_code() + { + return $this->error_code; + } + + public function get_error_string() + { + return $this->error_string; + } + + public function get_current_line() + { + return $this->current_line; + } + + public function get_current_column() + { + return $this->current_column; + } + + public function get_current_byte() + { + return $this->current_byte; + } + + public function get_data() + { + return $this->data; + } + + public function tag_open($parser, $tag, $attributes) + { + list($this->namespace[], $this->element[]) = $this->split_ns($tag); + + $attribs = array(); + foreach ($attributes as $name => $value) + { + list($attrib_namespace, $attribute) = $this->split_ns($name); + $attribs[$attrib_namespace][$attribute] = $value; + } + + if (isset($attribs[SIMPLEPIE_NAMESPACE_XML]['base'])) + { + $base = $this->registry->call('Misc', 'absolutize_url', array($attribs[SIMPLEPIE_NAMESPACE_XML]['base'], end($this->xml_base))); + if ($base !== false) + { + $this->xml_base[] = $base; + $this->xml_base_explicit[] = true; + } + } + else + { + $this->xml_base[] = end($this->xml_base); + $this->xml_base_explicit[] = end($this->xml_base_explicit); + } + + if (isset($attribs[SIMPLEPIE_NAMESPACE_XML]['lang'])) + { + $this->xml_lang[] = $attribs[SIMPLEPIE_NAMESPACE_XML]['lang']; + } + else + { + $this->xml_lang[] = end($this->xml_lang); + } + + if ($this->current_xhtml_construct >= 0) + { + $this->current_xhtml_construct++; + if (end($this->namespace) === SIMPLEPIE_NAMESPACE_XHTML) + { + $this->data['data'] .= '<' . end($this->element); + if (isset($attribs[''])) + { + foreach ($attribs[''] as $name => $value) + { + $this->data['data'] .= ' ' . $name . '="' . htmlspecialchars($value, ENT_COMPAT, $this->encoding) . '"'; + } + } + $this->data['data'] .= '>'; + } + } + else + { + $this->datas[] =& $this->data; + $this->data =& $this->data['child'][end($this->namespace)][end($this->element)][]; + $this->data = array('data' => '', 'attribs' => $attribs, 'xml_base' => end($this->xml_base), 'xml_base_explicit' => end($this->xml_base_explicit), 'xml_lang' => end($this->xml_lang)); + if ((end($this->namespace) === SIMPLEPIE_NAMESPACE_ATOM_03 && in_array(end($this->element), array('title', 'tagline', 'copyright', 'info', 'summary', 'content')) && isset($attribs['']['mode']) && $attribs['']['mode'] === 'xml') + || (end($this->namespace) === SIMPLEPIE_NAMESPACE_ATOM_10 && in_array(end($this->element), array('rights', 'subtitle', 'summary', 'info', 'title', 'content')) && isset($attribs['']['type']) && $attribs['']['type'] === 'xhtml') + || (end($this->namespace) === SIMPLEPIE_NAMESPACE_RSS_20 && in_array(end($this->element), array('title'))) + || (end($this->namespace) === SIMPLEPIE_NAMESPACE_RSS_090 && in_array(end($this->element), array('title'))) + || (end($this->namespace) === SIMPLEPIE_NAMESPACE_RSS_10 && in_array(end($this->element), array('title')))) + { + $this->current_xhtml_construct = 0; + } + } + } + + public function cdata($parser, $cdata) + { + if ($this->current_xhtml_construct >= 0) + { + $this->data['data'] .= htmlspecialchars($cdata, ENT_QUOTES, $this->encoding); + } + else + { + $this->data['data'] .= $cdata; + } + } + + public function tag_close($parser, $tag) + { + if ($this->current_xhtml_construct >= 0) + { + $this->current_xhtml_construct--; + if (end($this->namespace) === SIMPLEPIE_NAMESPACE_XHTML && !in_array(end($this->element), array('area', 'base', 'basefont', 'br', 'col', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param'))) + { + $this->data['data'] .= '</' . end($this->element) . '>'; + } + } + if ($this->current_xhtml_construct === -1) + { + $this->data =& $this->datas[count($this->datas) - 1]; + array_pop($this->datas); + } + + array_pop($this->element); + array_pop($this->namespace); + array_pop($this->xml_base); + array_pop($this->xml_base_explicit); + array_pop($this->xml_lang); + } + + public function split_ns($string) + { + static $cache = array(); + if (!isset($cache[$string])) + { + if ($pos = strpos($string, $this->separator)) + { + static $separator_length; + if (!$separator_length) + { + $separator_length = strlen($this->separator); + } + $namespace = substr($string, 0, $pos); + $local_name = substr($string, $pos + $separator_length); + if (strtolower($namespace) === SIMPLEPIE_NAMESPACE_ITUNES) + { + $namespace = SIMPLEPIE_NAMESPACE_ITUNES; + } + + // Normalize the Media RSS namespaces + if ($namespace === SIMPLEPIE_NAMESPACE_MEDIARSS_WRONG || + $namespace === SIMPLEPIE_NAMESPACE_MEDIARSS_WRONG2 || + $namespace === SIMPLEPIE_NAMESPACE_MEDIARSS_WRONG3 || + $namespace === SIMPLEPIE_NAMESPACE_MEDIARSS_WRONG4 || + $namespace === SIMPLEPIE_NAMESPACE_MEDIARSS_WRONG5 ) + { + $namespace = SIMPLEPIE_NAMESPACE_MEDIARSS; + } + $cache[$string] = array($namespace, $local_name); + } + else + { + $cache[$string] = array('', $string); + } + } + return $cache[$string]; + } + + private function parse_hcard($data, $category = false) { + $name = ''; + $link = ''; + // Check if h-card is set and pass that information on in the link. + if (isset($data['type']) && in_array('h-card', $data['type'])) { + if (isset($data['properties']['name'][0])) { + $name = $data['properties']['name'][0]; + } + if (isset($data['properties']['url'][0])) { + $link = $data['properties']['url'][0]; + if ($name === '') { + $name = $link; + } + else { + // can't have commas in categories. + $name = str_replace(',', '', $name); + } + $person_tag = $category ? '<span class="person-tag"></span>' : ''; + return '<a class="h-card" href="'.$link.'">'.$person_tag.$name.'</a>'; + } + } + return isset($data['value']) ? $data['value'] : ''; + } + + private function parse_microformats(&$data, $url) { + $feed_title = ''; + $feed_author = NULL; + $author_cache = array(); + $items = array(); + $entries = array(); + $mf = Mf2\parse($data, $url); + // First look for an h-feed. + $h_feed = array(); + foreach ($mf['items'] as $mf_item) { + if (in_array('h-feed', $mf_item['type'])) { + $h_feed = $mf_item; + break; + } + // Also look for an h-feed in the children of each top level item. + if (!isset($mf_item['children'][0]['type'])) continue; + if (in_array('h-feed', $mf_item['children'][0]['type'])) { + $h_feed = $mf_item['children'][0]; + // In this case the parent of the h-feed may be an h-card, so use it as + // the feed_author. + if (in_array('h-card', $mf_item['type'])) $feed_author = $mf_item; + break; + } + } + if (isset($h_feed['children'])) { + $entries = $h_feed['children']; + // Also set the feed title and store author from the h-feed if available. + if (isset($mf['items'][0]['properties']['name'][0])) { + $feed_title = $mf['items'][0]['properties']['name'][0]; + } + if (isset($mf['items'][0]['properties']['author'][0])) { + $feed_author = $mf['items'][0]['properties']['author'][0]; + } + } + else { + $entries = $mf['items']; + } + for ($i = 0; $i < count($entries); $i++) { + $entry = $entries[$i]; + if (in_array('h-entry', $entry['type'])) { + $item = array(); + $title = ''; + $description = ''; + if (isset($entry['properties']['url'][0])) { + $link = $entry['properties']['url'][0]; + if (isset($link['value'])) $link = $link['value']; + $item['link'] = array(array('data' => $link)); + } + if (isset($entry['properties']['uid'][0])) { + $guid = $entry['properties']['uid'][0]; + if (isset($guid['value'])) $guid = $guid['value']; + $item['guid'] = array(array('data' => $guid)); + } + if (isset($entry['properties']['name'][0])) { + $title = $entry['properties']['name'][0]; + if (isset($title['value'])) $title = $title['value']; + $item['title'] = array(array('data' => $title)); + } + if (isset($entry['properties']['author'][0]) || isset($feed_author)) { + // author is a special case, it can be plain text or an h-card array. + // If it's plain text it can also be a url that should be followed to + // get the actual h-card. + $author = isset($entry['properties']['author'][0]) ? + $entry['properties']['author'][0] : $feed_author; + if (!is_string($author)) { + $author = $this->parse_hcard($author); + } + else if (strpos($author, 'http') === 0) { + if (isset($author_cache[$author])) { + $author = $author_cache[$author]; + } + else { + $mf = Mf2\fetch($author); + foreach ($mf['items'] as $hcard) { + // Only interested in an h-card by itself in this case. + if (!in_array('h-card', $hcard['type'])) { + continue; + } + // It must have a url property matching what we fetched. + if (!isset($hcard['properties']['url']) || + !(in_array($author, $hcard['properties']['url']))) { + continue; + } + // Save parse_hcard the trouble of finding the correct url. + $hcard['properties']['url'][0] = $author; + // Cache this h-card for the next h-entry to check. + $author_cache[$author] = $this->parse_hcard($hcard); + $author = $author_cache[$author]; + break; + } + } + } + $item['author'] = array(array('data' => $author)); + } + if (isset($entry['properties']['photo'][0])) { + // If a photo is also in content, don't need to add it again here. + $content = ''; + if (isset($entry['properties']['content'][0]['html'])) { + $content = $entry['properties']['content'][0]['html']; + } + $photo_list = array(); + for ($j = 0; $j < count($entry['properties']['photo']); $j++) { + $photo = $entry['properties']['photo'][$j]; + if (strpos($content, $photo) === false) { + $photo_list[] = $photo; + } + } + // When there's more than one photo show the first and use a lightbox. + $count = count($photo_list); + if ($count > 1) { + $description = '<p>'; + for ($j = 0; $j < $count; $j++) { + $hidden = $j === 0 ? '' : 'class="hidden" '; + $description .= '<a href="'.$photo_list[$j].'" '.$hidden. + 'data-lightbox="image-set-'.$i.'">'. + '<img src="'.$photo_list[$j].'"></a>'; + } + $description .= '<br><b>'.$count.' photos</b></p>'; + } + else if ($count == 1) { + $description = '<p><img src="'.$photo_list[0].'"></p>'; + } + } + if (isset($entry['properties']['content'][0]['html'])) { + // e-content['value'] is the same as p-name when they are on the same + // element. Use this to replace title with a strip_tags version so + // that alt text from images is not included in the title. + if ($entry['properties']['content'][0]['value'] === $title) { + $title = strip_tags($entry['properties']['content'][0]['html']); + $item['title'] = array(array('data' => $title)); + } + $description .= $entry['properties']['content'][0]['html']; + if (isset($entry['properties']['in-reply-to'][0]['value'])) { + $in_reply_to = $entry['properties']['in-reply-to'][0]['value']; + $description .= '<p><span class="in-reply-to"></span> '. + '<a href="'.$in_reply_to.'">'.$in_reply_to.'</a><p>'; + } + $item['description'] = array(array('data' => $description)); + } + if (isset($entry['properties']['category'])) { + $category_csv = ''; + // Categories can also contain h-cards. + foreach ($entry['properties']['category'] as $category) { + if ($category_csv !== '') $category_csv .= ', '; + if (is_string($category)) { + // Can't have commas in categories. + $category_csv .= str_replace(',', '', $category); + } + else { + $category_csv .= $this->parse_hcard($category, true); + } + } + $item['category'] = array(array('data' => $category_csv)); + } + if (isset($entry['properties']['published'][0])) { + $timestamp = strtotime($entry['properties']['published'][0]); + $pub_date = date('F j Y g:ia', $timestamp).' GMT'; + $item['pubDate'] = array(array('data' => $pub_date)); + } + // The title and description are set to the empty string to represent + // a deleted item (which also makes it an invalid rss item). + if (isset($entry['properties']['deleted'][0])) { + $item['title'] = array(array('data' => '')); + $item['description'] = array(array('data' => '')); + } + $items[] = array('child' => array('' => $item)); + } + } + // Mimic RSS data format when storing microformats. + $link = array(array('data' => $url)); + $image = ''; + if (!is_string($feed_author) && + isset($feed_author['properties']['photo'][0])) { + $image = array(array('child' => array('' => array('url' => + array(array('data' => $feed_author['properties']['photo'][0])))))); + } + // Use the a name given for the h-feed, or get the title from the html. + if ($feed_title !== '') { + $feed_title = array(array('data' => htmlspecialchars($feed_title))); + } + else if ($position = strpos($data, '<title>')) { + $start = $position < 200 ? 0 : $position - 200; + $check = substr($data, $start, 400); + $matches = array(); + if (preg_match('/<title>(.+)<\/title>/', $check, $matches)) { + $feed_title = array(array('data' => htmlspecialchars($matches[1]))); + } + } + $channel = array('channel' => array(array('child' => array('' => + array('link' => $link, 'image' => $image, 'title' => $feed_title, + 'item' => $items))))); + $rss = array(array('attribs' => array('' => array('version' => '2.0')), + 'child' => array('' => $channel))); + $this->data = array('child' => array('' => array('rss' => $rss))); + return true; + } + + private function declare_html_entities() { + // This is required because the RSS specification says that entity-encoded + // html is allowed, but the xml specification says they must be declared. + return '<!DOCTYPE html [ <!ENTITY nbsp " "> <!ENTITY iexcl "¡"> <!ENTITY cent "¢"> <!ENTITY pound "£"> <!ENTITY curren "¤"> <!ENTITY yen "¥"> <!ENTITY brvbar "¦"> <!ENTITY sect "§"> <!ENTITY uml "¨"> <!ENTITY copy "©"> <!ENTITY ordf "ª"> <!ENTITY laquo "«"> <!ENTITY not "¬"> <!ENTITY shy "­"> <!ENTITY reg "®"> <!ENTITY macr "¯"> <!ENTITY deg "°"> <!ENTITY plusmn "±"> <!ENTITY sup2 "²"> <!ENTITY sup3 "³"> <!ENTITY acute "´"> <!ENTITY micro "µ"> <!ENTITY para "¶"> <!ENTITY middot "·"> <!ENTITY cedil "¸"> <!ENTITY sup1 "¹"> <!ENTITY ordm "º"> <!ENTITY raquo "»"> <!ENTITY frac14 "¼"> <!ENTITY frac12 "½"> <!ENTITY frac34 "¾"> <!ENTITY iquest "¿"> <!ENTITY Agrave "À"> <!ENTITY Aacute "Á"> <!ENTITY Acirc "Â"> <!ENTITY Atilde "Ã"> <!ENTITY Auml "Ä"> <!ENTITY Aring "Å"> <!ENTITY AElig "Æ"> <!ENTITY Ccedil "Ç"> <!ENTITY Egrave "È"> <!ENTITY Eacute "É"> <!ENTITY Ecirc "Ê"> <!ENTITY Euml "Ë"> <!ENTITY Igrave "Ì"> <!ENTITY Iacute "Í"> <!ENTITY Icirc "Î"> <!ENTITY Iuml "Ï"> <!ENTITY ETH "Ð"> <!ENTITY Ntilde "Ñ"> <!ENTITY Ograve "Ò"> <!ENTITY Oacute "Ó"> <!ENTITY Ocirc "Ô"> <!ENTITY Otilde "Õ"> <!ENTITY Ouml "Ö"> <!ENTITY times "×"> <!ENTITY Oslash "Ø"> <!ENTITY Ugrave "Ù"> <!ENTITY Uacute "Ú"> <!ENTITY Ucirc "Û"> <!ENTITY Uuml "Ü"> <!ENTITY Yacute "Ý"> <!ENTITY THORN "Þ"> <!ENTITY szlig "ß"> <!ENTITY agrave "à"> <!ENTITY aacute "á"> <!ENTITY acirc "â"> <!ENTITY atilde "ã"> <!ENTITY auml "ä"> <!ENTITY aring "å"> <!ENTITY aelig "æ"> <!ENTITY ccedil "ç"> <!ENTITY egrave "è"> <!ENTITY eacute "é"> <!ENTITY ecirc "ê"> <!ENTITY euml "ë"> <!ENTITY igrave "ì"> <!ENTITY iacute "í"> <!ENTITY icirc "î"> <!ENTITY iuml "ï"> <!ENTITY eth "ð"> <!ENTITY ntilde "ñ"> <!ENTITY ograve "ò"> <!ENTITY oacute "ó"> <!ENTITY ocirc "ô"> <!ENTITY otilde "õ"> <!ENTITY ouml "ö"> <!ENTITY divide "÷"> <!ENTITY oslash "ø"> <!ENTITY ugrave "ù"> <!ENTITY uacute "ú"> <!ENTITY ucirc "û"> <!ENTITY uuml "ü"> <!ENTITY yacute "ý"> <!ENTITY thorn "þ"> <!ENTITY yuml "ÿ"> <!ENTITY OElig "Œ"> <!ENTITY oelig "œ"> <!ENTITY Scaron "Š"> <!ENTITY scaron "š"> <!ENTITY Yuml "Ÿ"> <!ENTITY fnof "ƒ"> <!ENTITY circ "ˆ"> <!ENTITY tilde "˜"> <!ENTITY Alpha "Α"> <!ENTITY Beta "Β"> <!ENTITY Gamma "Γ"> <!ENTITY Epsilon "Ε"> <!ENTITY Zeta "Ζ"> <!ENTITY Eta "Η"> <!ENTITY Theta "Θ"> <!ENTITY Iota "Ι"> <!ENTITY Kappa "Κ"> <!ENTITY Lambda "Λ"> <!ENTITY Mu "Μ"> <!ENTITY Nu "Ν"> <!ENTITY Xi "Ξ"> <!ENTITY Omicron "Ο"> <!ENTITY Pi "Π"> <!ENTITY Rho "Ρ"> <!ENTITY Sigma "Σ"> <!ENTITY Tau "Τ"> <!ENTITY Upsilon "Υ"> <!ENTITY Phi "Φ"> <!ENTITY Chi "Χ"> <!ENTITY Psi "Ψ"> <!ENTITY Omega "Ω"> <!ENTITY alpha "α"> <!ENTITY beta "β"> <!ENTITY gamma "γ"> <!ENTITY delta "δ"> <!ENTITY epsilon "ε"> <!ENTITY zeta "ζ"> <!ENTITY eta "η"> <!ENTITY theta "θ"> <!ENTITY iota "ι"> <!ENTITY kappa "κ"> <!ENTITY lambda "λ"> <!ENTITY mu "μ"> <!ENTITY nu "ν"> <!ENTITY xi "ξ"> <!ENTITY omicron "ο"> <!ENTITY pi "π"> <!ENTITY rho "ρ"> <!ENTITY sigmaf "ς"> <!ENTITY sigma "σ"> <!ENTITY tau "τ"> <!ENTITY upsilon "υ"> <!ENTITY phi "φ"> <!ENTITY chi "χ"> <!ENTITY psi "ψ"> <!ENTITY omega "ω"> <!ENTITY thetasym "ϑ"> <!ENTITY upsih "ϒ"> <!ENTITY piv "ϖ"> <!ENTITY ensp " "> <!ENTITY emsp " "> <!ENTITY thinsp " "> <!ENTITY zwnj "‌"> <!ENTITY zwj "‍"> <!ENTITY lrm "‎"> <!ENTITY rlm "‏"> <!ENTITY ndash "–"> <!ENTITY mdash "—"> <!ENTITY lsquo "‘"> <!ENTITY rsquo "’"> <!ENTITY sbquo "‚"> <!ENTITY ldquo "“"> <!ENTITY rdquo "”"> <!ENTITY bdquo "„"> <!ENTITY dagger "†"> <!ENTITY Dagger "‡"> <!ENTITY bull "•"> <!ENTITY hellip "…"> <!ENTITY permil "‰"> <!ENTITY prime "′"> <!ENTITY Prime "″"> <!ENTITY lsaquo "‹"> <!ENTITY rsaquo "›"> <!ENTITY oline "‾"> <!ENTITY frasl "⁄"> <!ENTITY euro "€"> <!ENTITY image "ℑ"> <!ENTITY weierp "℘"> <!ENTITY real "ℜ"> <!ENTITY trade "™"> <!ENTITY alefsym "ℵ"> <!ENTITY larr "←"> <!ENTITY uarr "↑"> <!ENTITY rarr "→"> <!ENTITY darr "↓"> <!ENTITY harr "↔"> <!ENTITY crarr "↵"> <!ENTITY lArr "⇐"> <!ENTITY uArr "⇑"> <!ENTITY rArr "⇒"> <!ENTITY dArr "⇓"> <!ENTITY hArr "⇔"> <!ENTITY forall "∀"> <!ENTITY part "∂"> <!ENTITY exist "∃"> <!ENTITY empty "∅"> <!ENTITY nabla "∇"> <!ENTITY isin "∈"> <!ENTITY notin "∉"> <!ENTITY ni "∋"> <!ENTITY prod "∏"> <!ENTITY sum "∑"> <!ENTITY minus "−"> <!ENTITY lowast "∗"> <!ENTITY radic "√"> <!ENTITY prop "∝"> <!ENTITY infin "∞"> <!ENTITY ang "∠"> <!ENTITY and "∧"> <!ENTITY or "∨"> <!ENTITY cap "∩"> <!ENTITY cup "∪"> <!ENTITY int "∫"> <!ENTITY there4 "∴"> <!ENTITY sim "∼"> <!ENTITY cong "≅"> <!ENTITY asymp "≈"> <!ENTITY ne "≠"> <!ENTITY equiv "≡"> <!ENTITY le "≤"> <!ENTITY ge "≥"> <!ENTITY sub "⊂"> <!ENTITY sup "⊃"> <!ENTITY nsub "⊄"> <!ENTITY sube "⊆"> <!ENTITY supe "⊇"> <!ENTITY oplus "⊕"> <!ENTITY otimes "⊗"> <!ENTITY perp "⊥"> <!ENTITY sdot "⋅"> <!ENTITY lceil "⌈"> <!ENTITY rceil "⌉"> <!ENTITY lfloor "⌊"> <!ENTITY rfloor "⌋"> <!ENTITY lang "〈"> <!ENTITY rang "〉"> <!ENTITY loz "◊"> <!ENTITY spades "♠"> <!ENTITY clubs "♣"> <!ENTITY hearts "♥"> <!ENTITY diams "♦"> ]>'; + } +}
\ No newline at end of file diff --git a/vendor/simplepie/simplepie/library/SimplePie/Rating.php b/vendor/simplepie/simplepie/library/SimplePie/Rating.php new file mode 100644 index 000000000..eaf57080c --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Rating.php @@ -0,0 +1,128 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Handles `<media:rating>` or `<itunes:explicit>` tags as defined in Media RSS and iTunes RSS respectively + * + * Used by {@see SimplePie_Enclosure::get_rating()} and {@see SimplePie_Enclosure::get_ratings()} + * + * This class can be overloaded with {@see SimplePie::set_rating_class()} + * + * @package SimplePie + * @subpackage API + */ +class SimplePie_Rating +{ + /** + * Rating scheme + * + * @var string + * @see get_scheme() + */ + var $scheme; + + /** + * Rating value + * + * @var string + * @see get_value() + */ + var $value; + + /** + * Constructor, used to input the data + * + * For documentation on all the parameters, see the corresponding + * properties and their accessors + */ + public function __construct($scheme = null, $value = null) + { + $this->scheme = $scheme; + $this->value = $value; + } + + /** + * String-ified version + * + * @return string + */ + public function __toString() + { + // There is no $this->data here + return md5(serialize($this)); + } + + /** + * Get the organizational scheme for the rating + * + * @return string|null + */ + public function get_scheme() + { + if ($this->scheme !== null) + { + return $this->scheme; + } + else + { + return null; + } + } + + /** + * Get the value of the rating + * + * @return string|null + */ + public function get_value() + { + if ($this->value !== null) + { + return $this->value; + } + else + { + return null; + } + } +} diff --git a/vendor/simplepie/simplepie/library/SimplePie/Registry.php b/vendor/simplepie/simplepie/library/SimplePie/Registry.php new file mode 100755 index 000000000..e0909bb74 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Registry.php @@ -0,0 +1,224 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Handles creating objects and calling methods + * + * Access this via {@see SimplePie::get_registry()} + * + * @package SimplePie + */ +class SimplePie_Registry +{ + /** + * Default class mapping + * + * Overriding classes *must* subclass these. + * + * @var array + */ + protected $default = array( + 'Cache' => 'SimplePie_Cache', + 'Locator' => 'SimplePie_Locator', + 'Parser' => 'SimplePie_Parser', + 'File' => 'SimplePie_File', + 'Sanitize' => 'SimplePie_Sanitize', + 'Item' => 'SimplePie_Item', + 'Author' => 'SimplePie_Author', + 'Category' => 'SimplePie_Category', + 'Enclosure' => 'SimplePie_Enclosure', + 'Caption' => 'SimplePie_Caption', + 'Copyright' => 'SimplePie_Copyright', + 'Credit' => 'SimplePie_Credit', + 'Rating' => 'SimplePie_Rating', + 'Restriction' => 'SimplePie_Restriction', + 'Content_Type_Sniffer' => 'SimplePie_Content_Type_Sniffer', + 'Source' => 'SimplePie_Source', + 'Misc' => 'SimplePie_Misc', + 'XML_Declaration_Parser' => 'SimplePie_XML_Declaration_Parser', + 'Parse_Date' => 'SimplePie_Parse_Date', + ); + + /** + * Class mapping + * + * @see register() + * @var array + */ + protected $classes = array(); + + /** + * Legacy classes + * + * @see register() + * @var array + */ + protected $legacy = array(); + + /** + * Constructor + * + * No-op + */ + public function __construct() { } + + /** + * Register a class + * + * @param string $type See {@see $default} for names + * @param string $class Class name, must subclass the corresponding default + * @param bool $legacy Whether to enable legacy support for this class + * @return bool Successfulness + */ + public function register($type, $class, $legacy = false) + { + if (!@is_subclass_of($class, $this->default[$type])) + { + return false; + } + + $this->classes[$type] = $class; + + if ($legacy) + { + $this->legacy[] = $class; + } + + return true; + } + + /** + * Get the class registered for a type + * + * Where possible, use {@see create()} or {@see call()} instead + * + * @param string $type + * @return string|null + */ + public function get_class($type) + { + if (!empty($this->classes[$type])) + { + return $this->classes[$type]; + } + if (!empty($this->default[$type])) + { + return $this->default[$type]; + } + + return null; + } + + /** + * Create a new instance of a given type + * + * @param string $type + * @param array $parameters Parameters to pass to the constructor + * @return object Instance of class + */ + public function &create($type, $parameters = array()) + { + $class = $this->get_class($type); + + if (in_array($class, $this->legacy)) + { + switch ($type) + { + case 'locator': + // Legacy: file, timeout, useragent, file_class, max_checked_feeds, content_type_sniffer_class + // Specified: file, timeout, useragent, max_checked_feeds + $replacement = array($this->get_class('file'), $parameters[3], $this->get_class('content_type_sniffer')); + array_splice($parameters, 3, 1, $replacement); + break; + } + } + + if (!method_exists($class, '__construct')) + { + $instance = new $class; + } + else + { + $reflector = new ReflectionClass($class); + $instance = $reflector->newInstanceArgs($parameters); + } + + if (method_exists($instance, 'set_registry')) + { + $instance->set_registry($this); + } + return $instance; + } + + /** + * Call a static method for a type + * + * @param string $type + * @param string $method + * @param array $parameters + * @return mixed + */ + public function &call($type, $method, $parameters = array()) + { + $class = $this->get_class($type); + + if (in_array($class, $this->legacy)) + { + switch ($type) + { + case 'Cache': + // For backwards compatibility with old non-static + // Cache::create() methods + if ($method === 'get_handler') + { + $result = @call_user_func_array(array($class, 'create'), $parameters); + return $result; + } + break; + } + } + + $result = call_user_func_array(array($class, $method), $parameters); + return $result; + } +} diff --git a/vendor/simplepie/simplepie/library/SimplePie/Restriction.php b/vendor/simplepie/simplepie/library/SimplePie/Restriction.php new file mode 100644 index 000000000..001a5cd28 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Restriction.php @@ -0,0 +1,154 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Handles `<media:restriction>` as defined in Media RSS + * + * Used by {@see SimplePie_Enclosure::get_restriction()} and {@see SimplePie_Enclosure::get_restrictions()} + * + * This class can be overloaded with {@see SimplePie::set_restriction_class()} + * + * @package SimplePie + * @subpackage API + */ +class SimplePie_Restriction +{ + /** + * Relationship ('allow'/'deny') + * + * @var string + * @see get_relationship() + */ + var $relationship; + + /** + * Type of restriction + * + * @var string + * @see get_type() + */ + var $type; + + /** + * Restricted values + * + * @var string + * @see get_value() + */ + var $value; + + /** + * Constructor, used to input the data + * + * For documentation on all the parameters, see the corresponding + * properties and their accessors + */ + public function __construct($relationship = null, $type = null, $value = null) + { + $this->relationship = $relationship; + $this->type = $type; + $this->value = $value; + } + + /** + * String-ified version + * + * @return string + */ + public function __toString() + { + // There is no $this->data here + return md5(serialize($this)); + } + + /** + * Get the relationship + * + * @return string|null Either 'allow' or 'deny' + */ + public function get_relationship() + { + if ($this->relationship !== null) + { + return $this->relationship; + } + else + { + return null; + } + } + + /** + * Get the type + * + * @return string|null + */ + public function get_type() + { + if ($this->type !== null) + { + return $this->type; + } + else + { + return null; + } + } + + /** + * Get the list of restricted things + * + * @return string|null + */ + public function get_value() + { + if ($this->value !== null) + { + return $this->value; + } + else + { + return null; + } + } +} diff --git a/vendor/simplepie/simplepie/library/SimplePie/Sanitize.php b/vendor/simplepie/simplepie/library/SimplePie/Sanitize.php new file mode 100644 index 000000000..5a11721df --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Sanitize.php @@ -0,0 +1,591 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Used for data cleanup and post-processing + * + * + * This class can be overloaded with {@see SimplePie::set_sanitize_class()} + * + * @package SimplePie + * @todo Move to using an actual HTML parser (this will allow tags to be properly stripped, and to switch between HTML and XHTML), this will also make it easier to shorten a string while preserving HTML tags + */ +class SimplePie_Sanitize +{ + // Private vars + var $base; + + // Options + var $remove_div = true; + var $image_handler = ''; + var $strip_htmltags = array('base', 'blink', 'body', 'doctype', 'embed', 'font', 'form', 'frame', 'frameset', 'html', 'iframe', 'input', 'marquee', 'meta', 'noscript', 'object', 'param', 'script', 'style'); + var $encode_instead_of_strip = false; + var $strip_attributes = array('bgsound', 'expr', 'id', 'style', 'onclick', 'onerror', 'onfinish', 'onmouseover', 'onmouseout', 'onfocus', 'onblur', 'lowsrc', 'dynsrc'); + var $add_attributes = array('audio' => array('preload' => 'none'), 'iframe' => array('sandbox' => 'allow-scripts allow-same-origin'), 'video' => array('preload' => 'none')); + var $strip_comments = false; + var $output_encoding = 'UTF-8'; + var $enable_cache = true; + var $cache_location = './cache'; + var $cache_name_function = 'md5'; + var $timeout = 10; + var $useragent = ''; + var $force_fsockopen = false; + var $replace_url_attributes = null; + + public function __construct() + { + // Set defaults + $this->set_url_replacements(null); + } + + public function remove_div($enable = true) + { + $this->remove_div = (bool) $enable; + } + + public function set_image_handler($page = false) + { + if ($page) + { + $this->image_handler = (string) $page; + } + else + { + $this->image_handler = false; + } + } + + public function set_registry(SimplePie_Registry $registry) + { + $this->registry = $registry; + } + + public function pass_cache_data($enable_cache = true, $cache_location = './cache', $cache_name_function = 'md5', $cache_class = 'SimplePie_Cache') + { + if (isset($enable_cache)) + { + $this->enable_cache = (bool) $enable_cache; + } + + if ($cache_location) + { + $this->cache_location = (string) $cache_location; + } + + if ($cache_name_function) + { + $this->cache_name_function = (string) $cache_name_function; + } + } + + public function pass_file_data($file_class = 'SimplePie_File', $timeout = 10, $useragent = '', $force_fsockopen = false) + { + if ($timeout) + { + $this->timeout = (string) $timeout; + } + + if ($useragent) + { + $this->useragent = (string) $useragent; + } + + if ($force_fsockopen) + { + $this->force_fsockopen = (string) $force_fsockopen; + } + } + + public function strip_htmltags($tags = array('base', 'blink', 'body', 'doctype', 'embed', 'font', 'form', 'frame', 'frameset', 'html', 'iframe', 'input', 'marquee', 'meta', 'noscript', 'object', 'param', 'script', 'style')) + { + if ($tags) + { + if (is_array($tags)) + { + $this->strip_htmltags = $tags; + } + else + { + $this->strip_htmltags = explode(',', $tags); + } + } + else + { + $this->strip_htmltags = false; + } + } + + public function encode_instead_of_strip($encode = false) + { + $this->encode_instead_of_strip = (bool) $encode; + } + + public function strip_attributes($attribs = array('bgsound', 'expr', 'id', 'style', 'onclick', 'onerror', 'onfinish', 'onmouseover', 'onmouseout', 'onfocus', 'onblur', 'lowsrc', 'dynsrc')) + { + if ($attribs) + { + if (is_array($attribs)) + { + $this->strip_attributes = $attribs; + } + else + { + $this->strip_attributes = explode(',', $attribs); + } + } + else + { + $this->strip_attributes = false; + } + } + + public function add_attributes($attribs = array('audio' => array('preload' => 'none'), 'iframe' => array('sandbox' => 'allow-scripts allow-same-origin'), 'video' => array('preload' => 'none'))) + { + if ($attribs) + { + if (is_array($attribs)) + { + $this->add_attributes = $attribs; + } + else + { + $this->add_attributes = explode(',', $attribs); + } + } + else + { + $this->add_attributes = false; + } + } + + public function strip_comments($strip = false) + { + $this->strip_comments = (bool) $strip; + } + + public function set_output_encoding($encoding = 'UTF-8') + { + $this->output_encoding = (string) $encoding; + } + + /** + * Set element/attribute key/value pairs of HTML attributes + * containing URLs that need to be resolved relative to the feed + * + * Defaults to |a|@href, |area|@href, |blockquote|@cite, |del|@cite, + * |form|@action, |img|@longdesc, |img|@src, |input|@src, |ins|@cite, + * |q|@cite + * + * @since 1.0 + * @param array|null $element_attribute Element/attribute key/value pairs, null for default + */ + public function set_url_replacements($element_attribute = null) + { + if ($element_attribute === null) + { + $element_attribute = array( + 'a' => 'href', + 'area' => 'href', + 'blockquote' => 'cite', + 'del' => 'cite', + 'form' => 'action', + 'img' => array( + 'longdesc', + 'src' + ), + 'input' => 'src', + 'ins' => 'cite', + 'q' => 'cite' + ); + } + $this->replace_url_attributes = (array) $element_attribute; + } + + public function sanitize($data, $type, $base = '') + { + $data = trim($data); + if ($data !== '' || $type & SIMPLEPIE_CONSTRUCT_IRI) + { + if ($type & SIMPLEPIE_CONSTRUCT_MAYBE_HTML) + { + if (preg_match('/(&(#(x[0-9a-fA-F]+|[0-9]+)|[a-zA-Z0-9]+)|<\/[A-Za-z][^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3E]*' . SIMPLEPIE_PCRE_HTML_ATTRIBUTE . '>)/', $data)) + { + $type |= SIMPLEPIE_CONSTRUCT_HTML; + } + else + { + $type |= SIMPLEPIE_CONSTRUCT_TEXT; + } + } + + if ($type & SIMPLEPIE_CONSTRUCT_BASE64) + { + $data = base64_decode($data); + } + + if ($type & (SIMPLEPIE_CONSTRUCT_HTML | SIMPLEPIE_CONSTRUCT_XHTML)) + { + + if (!class_exists('DOMDocument')) + { + throw new SimplePie_Exception('DOMDocument not found, unable to use sanitizer'); + } + $document = new DOMDocument(); + $document->encoding = 'UTF-8'; + + $data = $this->preprocess($data, $type); + + set_error_handler(array('SimplePie_Misc', 'silence_errors')); + $document->loadHTML($data); + restore_error_handler(); + + $xpath = new DOMXPath($document); + + // Strip comments + if ($this->strip_comments) + { + $comments = $xpath->query('//comment()'); + + foreach ($comments as $comment) + { + $comment->parentNode->removeChild($comment); + } + } + + // Strip out HTML tags and attributes that might cause various security problems. + // Based on recommendations by Mark Pilgrim at: + // http://diveintomark.org/archives/2003/06/12/how_to_consume_rss_safely + if ($this->strip_htmltags) + { + foreach ($this->strip_htmltags as $tag) + { + $this->strip_tag($tag, $document, $xpath, $type); + } + } + + if ($this->strip_attributes) + { + foreach ($this->strip_attributes as $attrib) + { + $this->strip_attr($attrib, $xpath); + } + } + + if ($this->add_attributes) + { + foreach ($this->add_attributes as $tag => $valuePairs) + { + $this->add_attr($tag, $valuePairs, $document); + } + } + + // Replace relative URLs + $this->base = $base; + foreach ($this->replace_url_attributes as $element => $attributes) + { + $this->replace_urls($document, $element, $attributes); + } + + // If image handling (caching, etc.) is enabled, cache and rewrite all the image tags. + if (isset($this->image_handler) && ((string) $this->image_handler) !== '' && $this->enable_cache) + { + $images = $document->getElementsByTagName('img'); + foreach ($images as $img) + { + if ($img->hasAttribute('src')) + { + $image_url = call_user_func($this->cache_name_function, $img->getAttribute('src')); + $cache = $this->registry->call('Cache', 'get_handler', array($this->cache_location, $image_url, 'spi')); + + if ($cache->load()) + { + $img->setAttribute('src', $this->image_handler . $image_url); + } + else + { + $file = $this->registry->create('File', array($img->getAttribute('src'), $this->timeout, 5, array('X-FORWARDED-FOR' => $_SERVER['REMOTE_ADDR']), $this->useragent, $this->force_fsockopen)); + $headers = $file->headers; + + if ($file->success && ($file->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0 || ($file->status_code === 200 || $file->status_code > 206 && $file->status_code < 300))) + { + if ($cache->save(array('headers' => $file->headers, 'body' => $file->body))) + { + $img->setAttribute('src', $this->image_handler . $image_url); + } + else + { + trigger_error("$this->cache_location is not writeable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING); + } + } + } + } + } + } + + // Get content node + $div = $document->getElementsByTagName('body')->item(0)->firstChild; + // Finally, convert to a HTML string + if (version_compare(PHP_VERSION, '5.3.6', '>=')) + { + $data = trim($document->saveHTML($div)); + } + else + { + $data = trim($document->saveXML($div)); + } + + if ($this->remove_div) + { + $data = preg_replace('/^<div' . SIMPLEPIE_PCRE_XML_ATTRIBUTE . '>/', '', $data); + $data = preg_replace('/<\/div>$/', '', $data); + } + else + { + $data = preg_replace('/^<div' . SIMPLEPIE_PCRE_XML_ATTRIBUTE . '>/', '<div>', $data); + } + } + + if ($type & SIMPLEPIE_CONSTRUCT_IRI) + { + $absolute = $this->registry->call('Misc', 'absolutize_url', array($data, $base)); + if ($absolute !== false) + { + $data = $absolute; + } + } + + if ($type & (SIMPLEPIE_CONSTRUCT_TEXT | SIMPLEPIE_CONSTRUCT_IRI)) + { + $data = htmlspecialchars($data, ENT_COMPAT, 'UTF-8'); + } + + if ($this->output_encoding !== 'UTF-8') + { + $data = $this->registry->call('Misc', 'change_encoding', array($data, 'UTF-8', $this->output_encoding)); + } + } + return $data; + } + + protected function preprocess($html, $type) + { + $ret = ''; + $html = preg_replace('%</?(?:html|body)[^>]*?'.'>%is', '', $html); + if ($type & ~SIMPLEPIE_CONSTRUCT_XHTML) + { + // Atom XHTML constructs are wrapped with a div by default + // Note: No protection if $html contains a stray </div>! + $html = '<div>' . $html . '</div>'; + $ret .= '<!DOCTYPE html>'; + $content_type = 'text/html'; + } + else + { + $ret .= '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'; + $content_type = 'application/xhtml+xml'; + } + + $ret .= '<html><head>'; + $ret .= '<meta http-equiv="Content-Type" content="' . $content_type . '; charset=utf-8" />'; + $ret .= '</head><body>' . $html . '</body></html>'; + return $ret; + } + + public function replace_urls($document, $tag, $attributes) + { + if (!is_array($attributes)) + { + $attributes = array($attributes); + } + + if (!is_array($this->strip_htmltags) || !in_array($tag, $this->strip_htmltags)) + { + $elements = $document->getElementsByTagName($tag); + foreach ($elements as $element) + { + foreach ($attributes as $attribute) + { + if ($element->hasAttribute($attribute)) + { + $value = $this->registry->call('Misc', 'absolutize_url', array($element->getAttribute($attribute), $this->base)); + if ($value !== false) + { + $element->setAttribute($attribute, $value); + } + } + } + } + } + } + + public function do_strip_htmltags($match) + { + if ($this->encode_instead_of_strip) + { + if (isset($match[4]) && !in_array(strtolower($match[1]), array('script', 'style'))) + { + $match[1] = htmlspecialchars($match[1], ENT_COMPAT, 'UTF-8'); + $match[2] = htmlspecialchars($match[2], ENT_COMPAT, 'UTF-8'); + return "<$match[1]$match[2]>$match[3]</$match[1]>"; + } + else + { + return htmlspecialchars($match[0], ENT_COMPAT, 'UTF-8'); + } + } + elseif (isset($match[4]) && !in_array(strtolower($match[1]), array('script', 'style'))) + { + return $match[4]; + } + else + { + return ''; + } + } + + protected function strip_tag($tag, $document, $xpath, $type) + { + $elements = $xpath->query('body//' . $tag); + if ($this->encode_instead_of_strip) + { + foreach ($elements as $element) + { + $fragment = $document->createDocumentFragment(); + + // For elements which aren't script or style, include the tag itself + if (!in_array($tag, array('script', 'style'))) + { + $text = '<' . $tag; + if ($element->hasAttributes()) + { + $attrs = array(); + foreach ($element->attributes as $name => $attr) + { + $value = $attr->value; + + // In XHTML, empty values should never exist, so we repeat the value + if (empty($value) && ($type & SIMPLEPIE_CONSTRUCT_XHTML)) + { + $value = $name; + } + // For HTML, empty is fine + elseif (empty($value) && ($type & SIMPLEPIE_CONSTRUCT_HTML)) + { + $attrs[] = $name; + continue; + } + + // Standard attribute text + $attrs[] = $name . '="' . $attr->value . '"'; + } + $text .= ' ' . implode(' ', $attrs); + } + $text .= '>'; + $fragment->appendChild(new DOMText($text)); + } + + $number = $element->childNodes->length; + for ($i = $number; $i > 0; $i--) + { + $child = $element->childNodes->item(0); + $fragment->appendChild($child); + } + + if (!in_array($tag, array('script', 'style'))) + { + $fragment->appendChild(new DOMText('</' . $tag . '>')); + } + + $element->parentNode->replaceChild($fragment, $element); + } + + return; + } + elseif (in_array($tag, array('script', 'style'))) + { + foreach ($elements as $element) + { + $element->parentNode->removeChild($element); + } + + return; + } + else + { + foreach ($elements as $element) + { + $fragment = $document->createDocumentFragment(); + $number = $element->childNodes->length; + for ($i = $number; $i > 0; $i--) + { + $child = $element->childNodes->item(0); + $fragment->appendChild($child); + } + + $element->parentNode->replaceChild($fragment, $element); + } + } + } + + protected function strip_attr($attrib, $xpath) + { + $elements = $xpath->query('//*[@' . $attrib . ']'); + + foreach ($elements as $element) + { + $element->removeAttribute($attrib); + } + } + + protected function add_attr($tag, $valuePairs, $document) + { + $elements = $document->getElementsByTagName($tag); + foreach ($elements as $element) + { + foreach ($valuePairs as $attrib => $value) + { + $element->setAttribute($attrib, $value); + } + } + } +} diff --git a/vendor/simplepie/simplepie/library/SimplePie/Source.php b/vendor/simplepie/simplepie/library/SimplePie/Source.php new file mode 100644 index 000000000..1a66a392d --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/Source.php @@ -0,0 +1,610 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Handles `<atom:source>` + * + * Used by {@see SimplePie_Item::get_source()} + * + * This class can be overloaded with {@see SimplePie::set_source_class()} + * + * @package SimplePie + * @subpackage API + */ +class SimplePie_Source +{ + var $item; + var $data = array(); + protected $registry; + + public function __construct($item, $data) + { + $this->item = $item; + $this->data = $data; + } + + public function set_registry(SimplePie_Registry $registry) + { + $this->registry = $registry; + } + + public function __toString() + { + return md5(serialize($this->data)); + } + + public function get_source_tags($namespace, $tag) + { + if (isset($this->data['child'][$namespace][$tag])) + { + return $this->data['child'][$namespace][$tag]; + } + else + { + return null; + } + } + + public function get_base($element = array()) + { + return $this->item->get_base($element); + } + + public function sanitize($data, $type, $base = '') + { + return $this->item->sanitize($data, $type, $base); + } + + public function get_item() + { + return $this->item; + } + + public function get_title() + { + if ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'title')) + { + return $this->sanitize($return[0]['data'], $this->registry->call('Misc', 'atom_10_construct_type', array($return[0]['attribs'])), $this->get_base($return[0])); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'title')) + { + return $this->sanitize($return[0]['data'], $this->registry->call('Misc', 'atom_03_construct_type', array($return[0]['attribs'])), $this->get_base($return[0])); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'title')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'title')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'title')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_DC_11, 'title')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_DC_10, 'title')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + return null; + } + } + + public function get_category($key = 0) + { + $categories = $this->get_categories(); + if (isset($categories[$key])) + { + return $categories[$key]; + } + else + { + return null; + } + } + + public function get_categories() + { + $categories = array(); + + foreach ((array) $this->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'category') as $category) + { + $term = null; + $scheme = null; + $label = null; + if (isset($category['attribs']['']['term'])) + { + $term = $this->sanitize($category['attribs']['']['term'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['scheme'])) + { + $scheme = $this->sanitize($category['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($category['attribs']['']['label'])) + { + $label = $this->sanitize($category['attribs']['']['label'], SIMPLEPIE_CONSTRUCT_TEXT); + } + $categories[] = $this->registry->create('Category', array($term, $scheme, $label)); + } + foreach ((array) $this->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'category') as $category) + { + // This is really the label, but keep this as the term also for BC. + // Label will also work on retrieving because that falls back to term. + $term = $this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_TEXT); + if (isset($category['attribs']['']['domain'])) + { + $scheme = $this->sanitize($category['attribs']['']['domain'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $scheme = null; + } + $categories[] = $this->registry->create('Category', array($term, $scheme, null)); + } + foreach ((array) $this->get_source_tags(SIMPLEPIE_NAMESPACE_DC_11, 'subject') as $category) + { + $categories[] = $this->registry->create('Category', array($this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_TEXT), null, null)); + } + foreach ((array) $this->get_source_tags(SIMPLEPIE_NAMESPACE_DC_10, 'subject') as $category) + { + $categories[] = $this->registry->create('Category', array($this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_TEXT), null, null)); + } + + if (!empty($categories)) + { + return array_unique($categories); + } + else + { + return null; + } + } + + public function get_author($key = 0) + { + $authors = $this->get_authors(); + if (isset($authors[$key])) + { + return $authors[$key]; + } + else + { + return null; + } + } + + public function get_authors() + { + $authors = array(); + foreach ((array) $this->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'author') as $author) + { + $name = null; + $uri = null; + $email = null; + if (isset($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data'])) + { + $name = $this->sanitize($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data'])) + { + $uri = $this->sanitize($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0])); + } + if (isset($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['email'][0]['data'])) + { + $email = $this->sanitize($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['email'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $uri !== null) + { + $authors[] = $this->registry->create('Author', array($name, $uri, $email)); + } + } + if ($author = $this->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'author')) + { + $name = null; + $url = null; + $email = null; + if (isset($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['name'][0]['data'])) + { + $name = $this->sanitize($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['name'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0]['data'])) + { + $url = $this->sanitize($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0])); + } + if (isset($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['email'][0]['data'])) + { + $email = $this->sanitize($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['email'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $url !== null) + { + $authors[] = $this->registry->create('Author', array($name, $url, $email)); + } + } + foreach ((array) $this->get_source_tags(SIMPLEPIE_NAMESPACE_DC_11, 'creator') as $author) + { + $authors[] = $this->registry->create('Author', array($this->sanitize($author['data'], SIMPLEPIE_CONSTRUCT_TEXT), null, null)); + } + foreach ((array) $this->get_source_tags(SIMPLEPIE_NAMESPACE_DC_10, 'creator') as $author) + { + $authors[] = $this->registry->create('Author', array($this->sanitize($author['data'], SIMPLEPIE_CONSTRUCT_TEXT), null, null)); + } + foreach ((array) $this->get_source_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'author') as $author) + { + $authors[] = $this->registry->create('Author', array($this->sanitize($author['data'], SIMPLEPIE_CONSTRUCT_TEXT), null, null)); + } + + if (!empty($authors)) + { + return array_unique($authors); + } + else + { + return null; + } + } + + public function get_contributor($key = 0) + { + $contributors = $this->get_contributors(); + if (isset($contributors[$key])) + { + return $contributors[$key]; + } + else + { + return null; + } + } + + public function get_contributors() + { + $contributors = array(); + foreach ((array) $this->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'contributor') as $contributor) + { + $name = null; + $uri = null; + $email = null; + if (isset($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data'])) + { + $name = $this->sanitize($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data'])) + { + $uri = $this->sanitize($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0])); + } + if (isset($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['email'][0]['data'])) + { + $email = $this->sanitize($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['email'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $uri !== null) + { + $contributors[] = $this->registry->create('Author', array($name, $uri, $email)); + } + } + foreach ((array) $this->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'contributor') as $contributor) + { + $name = null; + $url = null; + $email = null; + if (isset($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['name'][0]['data'])) + { + $name = $this->sanitize($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['name'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if (isset($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0]['data'])) + { + $url = $this->sanitize($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0])); + } + if (isset($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['email'][0]['data'])) + { + $email = $this->sanitize($contributor['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['email'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + if ($name !== null || $email !== null || $url !== null) + { + $contributors[] = $this->registry->create('Author', array($name, $url, $email)); + } + } + + if (!empty($contributors)) + { + return array_unique($contributors); + } + else + { + return null; + } + } + + public function get_link($key = 0, $rel = 'alternate') + { + $links = $this->get_links($rel); + if (isset($links[$key])) + { + return $links[$key]; + } + else + { + return null; + } + } + + /** + * Added for parity between the parent-level and the item/entry-level. + */ + public function get_permalink() + { + return $this->get_link(0); + } + + public function get_links($rel = 'alternate') + { + if (!isset($this->data['links'])) + { + $this->data['links'] = array(); + if ($links = $this->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'link')) + { + foreach ($links as $link) + { + if (isset($link['attribs']['']['href'])) + { + $link_rel = (isset($link['attribs']['']['rel'])) ? $link['attribs']['']['rel'] : 'alternate'; + $this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($link)); + } + } + } + if ($links = $this->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'link')) + { + foreach ($links as $link) + { + if (isset($link['attribs']['']['href'])) + { + $link_rel = (isset($link['attribs']['']['rel'])) ? $link['attribs']['']['rel'] : 'alternate'; + $this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($link)); + + } + } + } + if ($links = $this->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'link')) + { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($links[0])); + } + if ($links = $this->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'link')) + { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($links[0])); + } + if ($links = $this->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'link')) + { + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($links[0])); + } + + $keys = array_keys($this->data['links']); + foreach ($keys as $key) + { + if ($this->registry->call('Misc', 'is_isegment_nz_nc', array($key))) + { + if (isset($this->data['links'][SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY . $key])) + { + $this->data['links'][SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY . $key] = array_merge($this->data['links'][$key], $this->data['links'][SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY . $key]); + $this->data['links'][$key] =& $this->data['links'][SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY . $key]; + } + else + { + $this->data['links'][SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY . $key] =& $this->data['links'][$key]; + } + } + elseif (substr($key, 0, 41) === SIMPLEPIE_IANA_LINK_RELATIONS_REGISTRY) + { + $this->data['links'][substr($key, 41)] =& $this->data['links'][$key]; + } + $this->data['links'][$key] = array_unique($this->data['links'][$key]); + } + } + + if (isset($this->data['links'][$rel])) + { + return $this->data['links'][$rel]; + } + else + { + return null; + } + } + + public function get_description() + { + if ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'subtitle')) + { + return $this->sanitize($return[0]['data'], $this->registry->call('Misc', 'atom_10_construct_type', array($return[0]['attribs'])), $this->get_base($return[0])); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'tagline')) + { + return $this->sanitize($return[0]['data'], $this->registry->call('Misc', 'atom_03_construct_type', array($return[0]['attribs'])), $this->get_base($return[0])); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'description')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'description')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'description')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_DC_11, 'description')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_DC_10, 'description')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'summary')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_HTML, $this->get_base($return[0])); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'subtitle')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_HTML, $this->get_base($return[0])); + } + else + { + return null; + } + } + + public function get_copyright() + { + if ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'rights')) + { + return $this->sanitize($return[0]['data'], $this->registry->call('Misc', 'atom_10_construct_type', array($return[0]['attribs'])), $this->get_base($return[0])); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'copyright')) + { + return $this->sanitize($return[0]['data'], $this->registry->call('Misc', 'atom_03_construct_type', array($return[0]['attribs'])), $this->get_base($return[0])); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'copyright')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_DC_11, 'rights')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_DC_10, 'rights')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + return null; + } + } + + public function get_language() + { + if ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'language')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_DC_11, 'language')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_DC_10, 'language')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + } + elseif (isset($this->data['xml_lang'])) + { + return $this->sanitize($this->data['xml_lang'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + return null; + } + } + + public function get_latitude() + { + if ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_W3C_BASIC_GEO, 'lat')) + { + return (float) $return[0]['data']; + } + elseif (($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_GEORSS, 'point')) && preg_match('/^((?:-)?[0-9]+(?:\.[0-9]+)) ((?:-)?[0-9]+(?:\.[0-9]+))$/', trim($return[0]['data']), $match)) + { + return (float) $match[1]; + } + else + { + return null; + } + } + + public function get_longitude() + { + if ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_W3C_BASIC_GEO, 'long')) + { + return (float) $return[0]['data']; + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_W3C_BASIC_GEO, 'lon')) + { + return (float) $return[0]['data']; + } + elseif (($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_GEORSS, 'point')) && preg_match('/^((?:-)?[0-9]+(?:\.[0-9]+)) ((?:-)?[0-9]+(?:\.[0-9]+))$/', trim($return[0]['data']), $match)) + { + return (float) $match[2]; + } + else + { + return null; + } + } + + public function get_image_url() + { + if ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'image')) + { + return $this->sanitize($return[0]['attribs']['']['href'], SIMPLEPIE_CONSTRUCT_IRI); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'logo')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($return[0])); + } + elseif ($return = $this->get_source_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'icon')) + { + return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($return[0])); + } + else + { + return null; + } + } +} + diff --git a/vendor/simplepie/simplepie/library/SimplePie/XML/Declaration/Parser.php b/vendor/simplepie/simplepie/library/SimplePie/XML/Declaration/Parser.php new file mode 100644 index 000000000..99e751672 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/XML/Declaration/Parser.php @@ -0,0 +1,361 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + + +/** + * Parses the XML Declaration + * + * @package SimplePie + * @subpackage Parsing + */ +class SimplePie_XML_Declaration_Parser +{ + /** + * XML Version + * + * @access public + * @var string + */ + var $version = '1.0'; + + /** + * Encoding + * + * @access public + * @var string + */ + var $encoding = 'UTF-8'; + + /** + * Standalone + * + * @access public + * @var bool + */ + var $standalone = false; + + /** + * Current state of the state machine + * + * @access private + * @var string + */ + var $state = 'before_version_name'; + + /** + * Input data + * + * @access private + * @var string + */ + var $data = ''; + + /** + * Input data length (to avoid calling strlen() everytime this is needed) + * + * @access private + * @var int + */ + var $data_length = 0; + + /** + * Current position of the pointer + * + * @var int + * @access private + */ + var $position = 0; + + /** + * Create an instance of the class with the input data + * + * @access public + * @param string $data Input data + */ + public function __construct($data) + { + $this->data = $data; + $this->data_length = strlen($this->data); + } + + /** + * Parse the input data + * + * @access public + * @return bool true on success, false on failure + */ + public function parse() + { + while ($this->state && $this->state !== 'emit' && $this->has_data()) + { + $state = $this->state; + $this->$state(); + } + $this->data = ''; + if ($this->state === 'emit') + { + return true; + } + else + { + $this->version = ''; + $this->encoding = ''; + $this->standalone = ''; + return false; + } + } + + /** + * Check whether there is data beyond the pointer + * + * @access private + * @return bool true if there is further data, false if not + */ + public function has_data() + { + return (bool) ($this->position < $this->data_length); + } + + /** + * Advance past any whitespace + * + * @return int Number of whitespace characters passed + */ + public function skip_whitespace() + { + $whitespace = strspn($this->data, "\x09\x0A\x0D\x20", $this->position); + $this->position += $whitespace; + return $whitespace; + } + + /** + * Read value + */ + public function get_value() + { + $quote = substr($this->data, $this->position, 1); + if ($quote === '"' || $quote === "'") + { + $this->position++; + $len = strcspn($this->data, $quote, $this->position); + if ($this->has_data()) + { + $value = substr($this->data, $this->position, $len); + $this->position += $len + 1; + return $value; + } + } + return false; + } + + public function before_version_name() + { + if ($this->skip_whitespace()) + { + $this->state = 'version_name'; + } + else + { + $this->state = false; + } + } + + public function version_name() + { + if (substr($this->data, $this->position, 7) === 'version') + { + $this->position += 7; + $this->skip_whitespace(); + $this->state = 'version_equals'; + } + else + { + $this->state = false; + } + } + + public function version_equals() + { + if (substr($this->data, $this->position, 1) === '=') + { + $this->position++; + $this->skip_whitespace(); + $this->state = 'version_value'; + } + else + { + $this->state = false; + } + } + + public function version_value() + { + if ($this->version = $this->get_value()) + { + $this->skip_whitespace(); + if ($this->has_data()) + { + $this->state = 'encoding_name'; + } + else + { + $this->state = 'emit'; + } + } + else + { + $this->state = false; + } + } + + public function encoding_name() + { + if (substr($this->data, $this->position, 8) === 'encoding') + { + $this->position += 8; + $this->skip_whitespace(); + $this->state = 'encoding_equals'; + } + else + { + $this->state = 'standalone_name'; + } + } + + public function encoding_equals() + { + if (substr($this->data, $this->position, 1) === '=') + { + $this->position++; + $this->skip_whitespace(); + $this->state = 'encoding_value'; + } + else + { + $this->state = false; + } + } + + public function encoding_value() + { + if ($this->encoding = $this->get_value()) + { + $this->skip_whitespace(); + if ($this->has_data()) + { + $this->state = 'standalone_name'; + } + else + { + $this->state = 'emit'; + } + } + else + { + $this->state = false; + } + } + + public function standalone_name() + { + if (substr($this->data, $this->position, 10) === 'standalone') + { + $this->position += 10; + $this->skip_whitespace(); + $this->state = 'standalone_equals'; + } + else + { + $this->state = false; + } + } + + public function standalone_equals() + { + if (substr($this->data, $this->position, 1) === '=') + { + $this->position++; + $this->skip_whitespace(); + $this->state = 'standalone_value'; + } + else + { + $this->state = false; + } + } + + public function standalone_value() + { + if ($standalone = $this->get_value()) + { + switch ($standalone) + { + case 'yes': + $this->standalone = true; + break; + + case 'no': + $this->standalone = false; + break; + + default: + $this->state = false; + return; + } + + $this->skip_whitespace(); + if ($this->has_data()) + { + $this->state = false; + } + else + { + $this->state = 'emit'; + } + } + else + { + $this->state = false; + } + } +} diff --git a/vendor/simplepie/simplepie/library/SimplePie/gzdecode.php b/vendor/simplepie/simplepie/library/SimplePie/gzdecode.php new file mode 100644 index 000000000..0e8bc8fc6 --- /dev/null +++ b/vendor/simplepie/simplepie/library/SimplePie/gzdecode.php @@ -0,0 +1,370 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + + +/** + * Decode 'gzip' encoded HTTP data + * + * @package SimplePie + * @subpackage HTTP + * @link http://www.gzip.org/format.txt + */ +class SimplePie_gzdecode +{ + /** + * Compressed data + * + * @access private + * @var string + * @see gzdecode::$data + */ + var $compressed_data; + + /** + * Size of compressed data + * + * @access private + * @var int + */ + var $compressed_size; + + /** + * Minimum size of a valid gzip string + * + * @access private + * @var int + */ + var $min_compressed_size = 18; + + /** + * Current position of pointer + * + * @access private + * @var int + */ + var $position = 0; + + /** + * Flags (FLG) + * + * @access private + * @var int + */ + var $flags; + + /** + * Uncompressed data + * + * @access public + * @see gzdecode::$compressed_data + * @var string + */ + var $data; + + /** + * Modified time + * + * @access public + * @var int + */ + var $MTIME; + + /** + * Extra Flags + * + * @access public + * @var int + */ + var $XFL; + + /** + * Operating System + * + * @access public + * @var int + */ + var $OS; + + /** + * Subfield ID 1 + * + * @access public + * @see gzdecode::$extra_field + * @see gzdecode::$SI2 + * @var string + */ + var $SI1; + + /** + * Subfield ID 2 + * + * @access public + * @see gzdecode::$extra_field + * @see gzdecode::$SI1 + * @var string + */ + var $SI2; + + /** + * Extra field content + * + * @access public + * @see gzdecode::$SI1 + * @see gzdecode::$SI2 + * @var string + */ + var $extra_field; + + /** + * Original filename + * + * @access public + * @var string + */ + var $filename; + + /** + * Human readable comment + * + * @access public + * @var string + */ + var $comment; + + /** + * Don't allow anything to be set + * + * @param string $name + * @param mixed $value + */ + public function __set($name, $value) + { + trigger_error("Cannot write property $name", E_USER_ERROR); + } + + /** + * Set the compressed string and related properties + * + * @param string $data + */ + public function __construct($data) + { + $this->compressed_data = $data; + $this->compressed_size = strlen($data); + } + + /** + * Decode the GZIP stream + * + * @return bool Successfulness + */ + public function parse() + { + if ($this->compressed_size >= $this->min_compressed_size) + { + // Check ID1, ID2, and CM + if (substr($this->compressed_data, 0, 3) !== "\x1F\x8B\x08") + { + return false; + } + + // Get the FLG (FLaGs) + $this->flags = ord($this->compressed_data[3]); + + // FLG bits above (1 << 4) are reserved + if ($this->flags > 0x1F) + { + return false; + } + + // Advance the pointer after the above + $this->position += 4; + + // MTIME + $mtime = substr($this->compressed_data, $this->position, 4); + // Reverse the string if we're on a big-endian arch because l is the only signed long and is machine endianness + if (current(unpack('S', "\x00\x01")) === 1) + { + $mtime = strrev($mtime); + } + $this->MTIME = current(unpack('l', $mtime)); + $this->position += 4; + + // Get the XFL (eXtra FLags) + $this->XFL = ord($this->compressed_data[$this->position++]); + + // Get the OS (Operating System) + $this->OS = ord($this->compressed_data[$this->position++]); + + // Parse the FEXTRA + if ($this->flags & 4) + { + // Read subfield IDs + $this->SI1 = $this->compressed_data[$this->position++]; + $this->SI2 = $this->compressed_data[$this->position++]; + + // SI2 set to zero is reserved for future use + if ($this->SI2 === "\x00") + { + return false; + } + + // Get the length of the extra field + $len = current(unpack('v', substr($this->compressed_data, $this->position, 2))); + $this->position += 2; + + // Check the length of the string is still valid + $this->min_compressed_size += $len + 4; + if ($this->compressed_size >= $this->min_compressed_size) + { + // Set the extra field to the given data + $this->extra_field = substr($this->compressed_data, $this->position, $len); + $this->position += $len; + } + else + { + return false; + } + } + + // Parse the FNAME + if ($this->flags & 8) + { + // Get the length of the filename + $len = strcspn($this->compressed_data, "\x00", $this->position); + + // Check the length of the string is still valid + $this->min_compressed_size += $len + 1; + if ($this->compressed_size >= $this->min_compressed_size) + { + // Set the original filename to the given string + $this->filename = substr($this->compressed_data, $this->position, $len); + $this->position += $len + 1; + } + else + { + return false; + } + } + + // Parse the FCOMMENT + if ($this->flags & 16) + { + // Get the length of the comment + $len = strcspn($this->compressed_data, "\x00", $this->position); + + // Check the length of the string is still valid + $this->min_compressed_size += $len + 1; + if ($this->compressed_size >= $this->min_compressed_size) + { + // Set the original comment to the given string + $this->comment = substr($this->compressed_data, $this->position, $len); + $this->position += $len + 1; + } + else + { + return false; + } + } + + // Parse the FHCRC + if ($this->flags & 2) + { + // Check the length of the string is still valid + $this->min_compressed_size += $len + 2; + if ($this->compressed_size >= $this->min_compressed_size) + { + // Read the CRC + $crc = current(unpack('v', substr($this->compressed_data, $this->position, 2))); + + // Check the CRC matches + if ((crc32(substr($this->compressed_data, 0, $this->position)) & 0xFFFF) === $crc) + { + $this->position += 2; + } + else + { + return false; + } + } + else + { + return false; + } + } + + // Decompress the actual data + if (($this->data = gzinflate(substr($this->compressed_data, $this->position, -8))) === false) + { + return false; + } + else + { + $this->position = $this->compressed_size - 8; + } + + // Check CRC of data + $crc = current(unpack('V', substr($this->compressed_data, $this->position, 4))); + $this->position += 4; + /*if (extension_loaded('hash') && sprintf('%u', current(unpack('V', hash('crc32b', $this->data)))) !== sprintf('%u', $crc)) + { + return false; + }*/ + + // Check ISIZE of data + $isize = current(unpack('V', substr($this->compressed_data, $this->position, 4))); + $this->position += 4; + if (sprintf('%u', strlen($this->data) & 0xFFFFFFFF) !== sprintf('%u', $isize)) + { + return false; + } + + // Wow, against all odds, we've actually got a valid gzip string + return true; + } + else + { + return false; + } + } +} |