diff --git a/library/Zend/Authentication/Adapter/AbstractAdapter.php b/library/Zend/Authentication/Adapter/AbstractAdapter.php new file mode 100755 index 0000000000..2f394f9e90 --- /dev/null +++ b/library/Zend/Authentication/Adapter/AbstractAdapter.php @@ -0,0 +1,71 @@ +credential; + } + + /** + * Sets the credential for binding + * + * @param mixed $credential + * @return AbstractAdapter + */ + public function setCredential($credential) + { + $this->credential = $credential; + + return $this; + } + + /** + * Returns the identity of the account being authenticated, or + * NULL if none is set. + * + * @return mixed + */ + public function getIdentity() + { + return $this->identity; + } + + /** + * Sets the identity for binding + * + * @param mixed $identity + * @return AbstractAdapter + */ + public function setIdentity($identity) + { + $this->identity = $identity; + + return $this; + } +} diff --git a/library/Zend/Authentication/Adapter/AdapterInterface.php b/library/Zend/Authentication/Adapter/AdapterInterface.php new file mode 100755 index 0000000000..d23d079366 --- /dev/null +++ b/library/Zend/Authentication/Adapter/AdapterInterface.php @@ -0,0 +1,21 @@ +zendDb = $zendDb; + + if (null !== $tableName) { + $this->setTableName($tableName); + } + + if (null !== $identityColumn) { + $this->setIdentityColumn($identityColumn); + } + + if (null !== $credentialColumn) { + $this->setCredentialColumn($credentialColumn); + } + } + + /** + * setTableName() - set the table name to be used in the select query + * + * @param string $tableName + * @return self Provides a fluent interface + */ + public function setTableName($tableName) + { + $this->tableName = $tableName; + return $this; + } + + /** + * setIdentityColumn() - set the column name to be used as the identity column + * + * @param string $identityColumn + * @return self Provides a fluent interface + */ + public function setIdentityColumn($identityColumn) + { + $this->identityColumn = $identityColumn; + return $this; + } + + /** + * setCredentialColumn() - set the column name to be used as the credential column + * + * @param string $credentialColumn + * @return self Provides a fluent interface + */ + public function setCredentialColumn($credentialColumn) + { + $this->credentialColumn = $credentialColumn; + return $this; + } + + /** + * setAmbiguityIdentity() - sets a flag for usage of identical identities + * with unique credentials. It accepts integers (0, 1) or boolean (true, + * false) parameters. Default is false. + * + * @param int|bool $flag + * @return self Provides a fluent interface + */ + public function setAmbiguityIdentity($flag) + { + if (is_int($flag)) { + $this->ambiguityIdentity = (1 === $flag ? true : false); + } elseif (is_bool($flag)) { + $this->ambiguityIdentity = $flag; + } + return $this; + } + + /** + * getAmbiguityIdentity() - returns TRUE for usage of multiple identical + * identities with different credentials, FALSE if not used. + * + * @return bool + */ + public function getAmbiguityIdentity() + { + return $this->ambiguityIdentity; + } + + /** + * getDbSelect() - Return the preauthentication Db Select object for userland select query modification + * + * @return Sql\Select + */ + public function getDbSelect() + { + if ($this->dbSelect == null) { + $this->dbSelect = new Sql\Select(); + } + return $this->dbSelect; + } + + /** + * getResultRowObject() - Returns the result row as a stdClass object + * + * @param string|array $returnColumns + * @param string|array $omitColumns + * @return stdClass|bool + */ + public function getResultRowObject($returnColumns = null, $omitColumns = null) + { + if (!$this->resultRow) { + return false; + } + + $returnObject = new stdClass(); + + if (null !== $returnColumns) { + $availableColumns = array_keys($this->resultRow); + foreach ((array) $returnColumns as $returnColumn) { + if (in_array($returnColumn, $availableColumns)) { + $returnObject->{$returnColumn} = $this->resultRow[$returnColumn]; + } + } + return $returnObject; + } elseif (null !== $omitColumns) { + $omitColumns = (array) $omitColumns; + foreach ($this->resultRow as $resultColumn => $resultValue) { + if (!in_array($resultColumn, $omitColumns)) { + $returnObject->{$resultColumn} = $resultValue; + } + } + return $returnObject; + } + + foreach ($this->resultRow as $resultColumn => $resultValue) { + $returnObject->{$resultColumn} = $resultValue; + } + return $returnObject; + } + + /** + * This method is called to attempt an authentication. Previous to this + * call, this adapter would have already been configured with all + * necessary information to successfully connect to a database table and + * attempt to find a record matching the provided identity. + * + * @throws Exception\RuntimeException if answering the authentication query is impossible + * @return AuthenticationResult + */ + public function authenticate() + { + $this->authenticateSetup(); + $dbSelect = $this->authenticateCreateSelect(); + $resultIdentities = $this->authenticateQuerySelect($dbSelect); + + if (($authResult = $this->authenticateValidateResultSet($resultIdentities)) instanceof AuthenticationResult) { + return $authResult; + } + + // At this point, ambiguity is already done. Loop, check and break on success. + foreach ($resultIdentities as $identity) { + $authResult = $this->authenticateValidateResult($identity); + if ($authResult->isValid()) { + break; + } + } + + return $authResult; + } + + /** + * _authenticateValidateResult() - This method attempts to validate that + * the record in the resultset is indeed a record that matched the + * identity provided to this adapter. + * + * @param array $resultIdentity + * @return AuthenticationResult + */ + abstract protected function authenticateValidateResult($resultIdentity); + + /** + * _authenticateCreateSelect() - This method creates a Zend\Db\Sql\Select object that + * is completely configured to be queried against the database. + * + * @return Sql\Select + */ + abstract protected function authenticateCreateSelect(); + + /** + * _authenticateSetup() - This method abstracts the steps involved with + * making sure that this adapter was indeed setup properly with all + * required pieces of information. + * + * @throws Exception\RuntimeException in the event that setup was not done properly + * @return bool + */ + protected function authenticateSetup() + { + $exception = null; + + if ($this->tableName == '') { + $exception = 'A table must be supplied for the DbTable authentication adapter.'; + } elseif ($this->identityColumn == '') { + $exception = 'An identity column must be supplied for the DbTable authentication adapter.'; + } elseif ($this->credentialColumn == '') { + $exception = 'A credential column must be supplied for the DbTable authentication adapter.'; + } elseif ($this->identity == '') { + $exception = 'A value for the identity was not provided prior to authentication with DbTable.'; + } elseif ($this->credential === null) { + $exception = 'A credential value was not provided prior to authentication with DbTable.'; + } + + if (null !== $exception) { + throw new Exception\RuntimeException($exception); + } + + $this->authenticateResultInfo = array( + 'code' => AuthenticationResult::FAILURE, + 'identity' => $this->identity, + 'messages' => array() + ); + + return true; + } + + /** + * _authenticateQuerySelect() - This method accepts a Zend\Db\Sql\Select object and + * performs a query against the database with that object. + * + * @param Sql\Select $dbSelect + * @throws Exception\RuntimeException when an invalid select object is encountered + * @return array + */ + protected function authenticateQuerySelect(Sql\Select $dbSelect) + { + $sql = new Sql\Sql($this->zendDb); + $statement = $sql->prepareStatementForSqlObject($dbSelect); + try { + $result = $statement->execute(); + $resultIdentities = array(); + // iterate result, most cross platform way + foreach ($result as $row) { + // ZF-6428 - account for db engines that by default return uppercase column names + if (isset($row['ZEND_AUTH_CREDENTIAL_MATCH'])) { + $row['zend_auth_credential_match'] = $row['ZEND_AUTH_CREDENTIAL_MATCH']; + unset($row['ZEND_AUTH_CREDENTIAL_MATCH']); + } + $resultIdentities[] = $row; + } + } catch (\Exception $e) { + throw new Exception\RuntimeException( + 'The supplied parameters to DbTable failed to ' + . 'produce a valid sql statement, please check table and column names ' + . 'for validity.', + 0, + $e + ); + } + return $resultIdentities; + } + + /** + * _authenticateValidateResultSet() - This method attempts to make + * certain that only one record was returned in the resultset + * + * @param array $resultIdentities + * @return bool|\Zend\Authentication\Result + */ + protected function authenticateValidateResultSet(array $resultIdentities) + { + if (count($resultIdentities) < 1) { + $this->authenticateResultInfo['code'] = AuthenticationResult::FAILURE_IDENTITY_NOT_FOUND; + $this->authenticateResultInfo['messages'][] = 'A record with the supplied identity could not be found.'; + return $this->authenticateCreateAuthResult(); + } elseif (count($resultIdentities) > 1 && false === $this->getAmbiguityIdentity()) { + $this->authenticateResultInfo['code'] = AuthenticationResult::FAILURE_IDENTITY_AMBIGUOUS; + $this->authenticateResultInfo['messages'][] = 'More than one record matches the supplied identity.'; + return $this->authenticateCreateAuthResult(); + } + + return true; + } + + /** + * Creates a Zend\Authentication\Result object from the information that + * has been collected during the authenticate() attempt. + * + * @return AuthenticationResult + */ + protected function authenticateCreateAuthResult() + { + return new AuthenticationResult( + $this->authenticateResultInfo['code'], + $this->authenticateResultInfo['identity'], + $this->authenticateResultInfo['messages'] + ); + } +} diff --git a/library/Zend/Authentication/Adapter/DbTable/CallbackCheckAdapter.php b/library/Zend/Authentication/Adapter/DbTable/CallbackCheckAdapter.php new file mode 100755 index 0000000000..d585e353eb --- /dev/null +++ b/library/Zend/Authentication/Adapter/DbTable/CallbackCheckAdapter.php @@ -0,0 +1,117 @@ +setCredentialValidationCallback($credentialValidationCallback); + } else { + $this->setCredentialValidationCallback(function ($a, $b) { + return $a === $b; + }); + } + } + + /** + * setCredentialValidationCallback() - allows the developer to use a callback as a way of checking the + * credential. + * + * @param callable $validationCallback + * @return self + * @throws Exception\InvalidArgumentException + */ + public function setCredentialValidationCallback($validationCallback) + { + if (!is_callable($validationCallback)) { + throw new Exception\InvalidArgumentException('Invalid callback provided'); + } + $this->credentialValidationCallback = $validationCallback; + return $this; + } + + /** + * _authenticateCreateSelect() - This method creates a Zend\Db\Sql\Select object that + * is completely configured to be queried against the database. + * + * @return Sql\Select + */ + protected function authenticateCreateSelect() + { + // get select + $dbSelect = clone $this->getDbSelect(); + $dbSelect->from($this->tableName) + ->columns(array(Sql\Select::SQL_STAR)) + ->where(new SqlOp($this->identityColumn, '=', $this->identity)); + + return $dbSelect; + } + + /** + * _authenticateValidateResult() - This method attempts to validate that + * the record in the resultset is indeed a record that matched the + * identity provided to this adapter. + * + * @param array $resultIdentity + * @return AuthenticationResult + */ + protected function authenticateValidateResult($resultIdentity) + { + try { + $callbackResult = call_user_func($this->credentialValidationCallback, $resultIdentity[$this->credentialColumn], $this->credential); + } catch (\Exception $e) { + $this->authenticateResultInfo['code'] = AuthenticationResult::FAILURE_UNCATEGORIZED; + $this->authenticateResultInfo['messages'][] = $e->getMessage(); + return $this->authenticateCreateAuthResult(); + } + if ($callbackResult !== true) { + $this->authenticateResultInfo['code'] = AuthenticationResult::FAILURE_CREDENTIAL_INVALID; + $this->authenticateResultInfo['messages'][] = 'Supplied credential is invalid.'; + return $this->authenticateCreateAuthResult(); + } + + $this->resultRow = $resultIdentity; + + $this->authenticateResultInfo['code'] = AuthenticationResult::SUCCESS; + $this->authenticateResultInfo['messages'][] = 'Authentication successful.'; + return $this->authenticateCreateAuthResult(); + } +} diff --git a/library/Zend/Authentication/Adapter/DbTable/CredentialTreatmentAdapter.php b/library/Zend/Authentication/Adapter/DbTable/CredentialTreatmentAdapter.php new file mode 100755 index 0000000000..b31cc5f74f --- /dev/null +++ b/library/Zend/Authentication/Adapter/DbTable/CredentialTreatmentAdapter.php @@ -0,0 +1,124 @@ +setCredentialTreatment($credentialTreatment); + } + } + + /** + * setCredentialTreatment() - allows the developer to pass a parametrized string that is + * used to transform or treat the input credential data. + * + * In many cases, passwords and other sensitive data are encrypted, hashed, encoded, + * obscured, or otherwise treated through some function or algorithm. By specifying a + * parametrized treatment string with this method, a developer may apply arbitrary SQL + * upon input credential data. + * + * Examples: + * + * 'PASSWORD(?)' + * 'MD5(?)' + * + * @param string $treatment + * @return self Provides a fluent interface + */ + public function setCredentialTreatment($treatment) + { + $this->credentialTreatment = $treatment; + return $this; + } + + /** + * _authenticateCreateSelect() - This method creates a Zend\Db\Sql\Select object that + * is completely configured to be queried against the database. + * + * @return Sql\Select + */ + protected function authenticateCreateSelect() + { + // build credential expression + if (empty($this->credentialTreatment) || (strpos($this->credentialTreatment, '?') === false)) { + $this->credentialTreatment = '?'; + } + + $credentialExpression = new SqlExpr( + '(CASE WHEN ?' . ' = ' . $this->credentialTreatment . ' THEN 1 ELSE 0 END) AS ?', + array($this->credentialColumn, $this->credential, 'zend_auth_credential_match'), + array(SqlExpr::TYPE_IDENTIFIER, SqlExpr::TYPE_VALUE, SqlExpr::TYPE_IDENTIFIER) + ); + + // get select + $dbSelect = clone $this->getDbSelect(); + $dbSelect->from($this->tableName) + ->columns(array('*', $credentialExpression)) + ->where(new SqlOp($this->identityColumn, '=', $this->identity)); + + return $dbSelect; + } + + /** + * _authenticateValidateResult() - This method attempts to validate that + * the record in the resultset is indeed a record that matched the + * identity provided to this adapter. + * + * @param array $resultIdentity + * @return AuthenticationResult + */ + protected function authenticateValidateResult($resultIdentity) + { + if ($resultIdentity['zend_auth_credential_match'] != '1') { + $this->authenticateResultInfo['code'] = AuthenticationResult::FAILURE_CREDENTIAL_INVALID; + $this->authenticateResultInfo['messages'][] = 'Supplied credential is invalid.'; + return $this->authenticateCreateAuthResult(); + } + + unset($resultIdentity['zend_auth_credential_match']); + $this->resultRow = $resultIdentity; + + $this->authenticateResultInfo['code'] = AuthenticationResult::SUCCESS; + $this->authenticateResultInfo['messages'][] = 'Authentication successful.'; + return $this->authenticateCreateAuthResult(); + } +} diff --git a/library/Zend/Authentication/Adapter/DbTable/Exception/ExceptionInterface.php b/library/Zend/Authentication/Adapter/DbTable/Exception/ExceptionInterface.php new file mode 100755 index 0000000000..5365808fcc --- /dev/null +++ b/library/Zend/Authentication/Adapter/DbTable/Exception/ExceptionInterface.php @@ -0,0 +1,16 @@ +setFilename($filename); + } + if ($realm !== null) { + $this->setRealm($realm); + } + if ($identity !== null) { + $this->setIdentity($identity); + } + if ($credential !== null) { + $this->setCredential($credential); + } + } + + /** + * Returns the filename option value or null if it has not yet been set + * + * @return string|null + */ + public function getFilename() + { + return $this->filename; + } + + /** + * Sets the filename option value + * + * @param mixed $filename + * @return Digest Provides a fluent interface + */ + public function setFilename($filename) + { + $this->filename = (string) $filename; + return $this; + } + + /** + * Returns the realm option value or null if it has not yet been set + * + * @return string|null + */ + public function getRealm() + { + return $this->realm; + } + + /** + * Sets the realm option value + * + * @param mixed $realm + * @return Digest Provides a fluent interface + */ + public function setRealm($realm) + { + $this->realm = (string) $realm; + return $this; + } + + /** + * Returns the username option value or null if it has not yet been set + * + * @return string|null + */ + public function getUsername() + { + return $this->getIdentity(); + } + + /** + * Sets the username option value + * + * @param mixed $username + * @return Digest Provides a fluent interface + */ + public function setUsername($username) + { + return $this->setIdentity($username); + } + + /** + * Returns the password option value or null if it has not yet been set + * + * @return string|null + */ + public function getPassword() + { + return $this->getCredential(); + } + + /** + * Sets the password option value + * + * @param mixed $password + * @return Digest Provides a fluent interface + */ + public function setPassword($password) + { + return $this->setCredential($password); + } + + /** + * Defined by Zend\Authentication\Adapter\AdapterInterface + * + * @throws Exception\ExceptionInterface + * @return AuthenticationResult + */ + public function authenticate() + { + $optionsRequired = array('filename', 'realm', 'identity', 'credential'); + foreach ($optionsRequired as $optionRequired) { + if (null === $this->$optionRequired) { + throw new Exception\RuntimeException("Option '$optionRequired' must be set before authentication"); + } + } + + ErrorHandler::start(E_WARNING); + $fileHandle = fopen($this->filename, 'r'); + $error = ErrorHandler::stop(); + if (false === $fileHandle) { + throw new Exception\UnexpectedValueException("Cannot open '$this->filename' for reading", 0, $error); + } + + $id = "$this->identity:$this->realm"; + $idLength = strlen($id); + + $result = array( + 'code' => AuthenticationResult::FAILURE, + 'identity' => array( + 'realm' => $this->realm, + 'username' => $this->identity, + ), + 'messages' => array() + ); + + while (($line = fgets($fileHandle)) !== false) { + $line = trim($line); + if (empty($line)) { + break; + } + if (substr($line, 0, $idLength) === $id) { + if (CryptUtils::compareStrings(substr($line, -32), md5("$this->identity:$this->realm:$this->credential"))) { + return new AuthenticationResult(AuthenticationResult::SUCCESS, $result['identity'], $result['messages']); + } + $result['messages'][] = 'Password incorrect'; + return new AuthenticationResult(AuthenticationResult::FAILURE_CREDENTIAL_INVALID, $result['identity'], $result['messages']); + } + } + + $result['messages'][] = "Username '$this->identity' and realm '$this->realm' combination not found"; + return new AuthenticationResult(AuthenticationResult::FAILURE_IDENTITY_NOT_FOUND, $result['identity'], $result['messages']); + } +} diff --git a/library/Zend/Authentication/Adapter/Exception/ExceptionInterface.php b/library/Zend/Authentication/Adapter/Exception/ExceptionInterface.php new file mode 100755 index 0000000000..4c9b9d4be1 --- /dev/null +++ b/library/Zend/Authentication/Adapter/Exception/ExceptionInterface.php @@ -0,0 +1,16 @@ + 'basic'|'digest'|'basic digest' + * 'realm' => + * 'digest_domains' => Space-delimited list of URIs + * 'nonce_timeout' => + * 'use_opaque' => Whether to send the opaque value in the header + * 'algorithm' => See $supportedAlgos. Default: MD5 + * 'proxy_auth' => Whether to do authentication as a Proxy + * @throws Exception\InvalidArgumentException + */ + public function __construct(array $config) + { + $this->request = null; + $this->response = null; + $this->ieNoOpaque = false; + + if (empty($config['accept_schemes'])) { + throw new Exception\InvalidArgumentException('Config key "accept_schemes" is required'); + } + + $schemes = explode(' ', $config['accept_schemes']); + $this->acceptSchemes = array_intersect($schemes, $this->supportedSchemes); + if (empty($this->acceptSchemes)) { + throw new Exception\InvalidArgumentException(sprintf( + 'No supported schemes given in "accept_schemes". Valid values: %s', + implode(', ', $this->supportedSchemes) + )); + } + + // Double-quotes are used to delimit the realm string in the HTTP header, + // and colons are field delimiters in the password file. + if (empty($config['realm']) || + !ctype_print($config['realm']) || + strpos($config['realm'], ':') !== false || + strpos($config['realm'], '"') !== false) { + throw new Exception\InvalidArgumentException( + 'Config key \'realm\' is required, and must contain only printable characters,' + . 'excluding quotation marks and colons' + ); + } else { + $this->realm = $config['realm']; + } + + if (in_array('digest', $this->acceptSchemes)) { + if (empty($config['digest_domains']) || + !ctype_print($config['digest_domains']) || + strpos($config['digest_domains'], '"') !== false) { + throw new Exception\InvalidArgumentException( + 'Config key \'digest_domains\' is required, and must contain ' + . 'only printable characters, excluding quotation marks' + ); + } else { + $this->domains = $config['digest_domains']; + } + + if (empty($config['nonce_timeout']) || + !is_numeric($config['nonce_timeout'])) { + throw new Exception\InvalidArgumentException( + 'Config key \'nonce_timeout\' is required, and must be an integer' + ); + } else { + $this->nonceTimeout = (int) $config['nonce_timeout']; + } + + // We use the opaque value unless explicitly told not to + if (isset($config['use_opaque']) && false == (bool) $config['use_opaque']) { + $this->useOpaque = false; + } else { + $this->useOpaque = true; + } + + if (isset($config['algorithm']) && in_array($config['algorithm'], $this->supportedAlgos)) { + $this->algo = $config['algorithm']; + } else { + $this->algo = 'MD5'; + } + } + + // Don't be a proxy unless explicitly told to do so + if (isset($config['proxy_auth']) && true == (bool) $config['proxy_auth']) { + $this->imaProxy = true; // I'm a Proxy + } else { + $this->imaProxy = false; + } + } + + /** + * Setter for the basicResolver property + * + * @param Http\ResolverInterface $resolver + * @return Http Provides a fluent interface + */ + public function setBasicResolver(Http\ResolverInterface $resolver) + { + $this->basicResolver = $resolver; + + return $this; + } + + /** + * Getter for the basicResolver property + * + * @return Http\ResolverInterface + */ + public function getBasicResolver() + { + return $this->basicResolver; + } + + /** + * Setter for the digestResolver property + * + * @param Http\ResolverInterface $resolver + * @return Http Provides a fluent interface + */ + public function setDigestResolver(Http\ResolverInterface $resolver) + { + $this->digestResolver = $resolver; + + return $this; + } + + /** + * Getter for the digestResolver property + * + * @return Http\ResolverInterface + */ + public function getDigestResolver() + { + return $this->digestResolver; + } + + /** + * Setter for the Request object + * + * @param HTTPRequest $request + * @return Http Provides a fluent interface + */ + public function setRequest(HTTPRequest $request) + { + $this->request = $request; + + return $this; + } + + /** + * Getter for the Request object + * + * @return HTTPRequest + */ + public function getRequest() + { + return $this->request; + } + + /** + * Setter for the Response object + * + * @param HTTPResponse $response + * @return Http Provides a fluent interface + */ + public function setResponse(HTTPResponse $response) + { + $this->response = $response; + + return $this; + } + + /** + * Getter for the Response object + * + * @return HTTPResponse + */ + public function getResponse() + { + return $this->response; + } + + /** + * Authenticate + * + * @throws Exception\RuntimeException + * @return Authentication\Result + */ + public function authenticate() + { + if (empty($this->request) || empty($this->response)) { + throw new Exception\RuntimeException( + 'Request and Response objects must be set before calling authenticate()' + ); + } + + if ($this->imaProxy) { + $getHeader = 'Proxy-Authorization'; + } else { + $getHeader = 'Authorization'; + } + + $headers = $this->request->getHeaders(); + if (!$headers->has($getHeader)) { + return $this->challengeClient(); + } + $authHeader = $headers->get($getHeader)->getFieldValue(); + if (!$authHeader) { + return $this->challengeClient(); + } + + list($clientScheme) = explode(' ', $authHeader); + $clientScheme = strtolower($clientScheme); + + // The server can issue multiple challenges, but the client should + // answer with only the selected auth scheme. + if (!in_array($clientScheme, $this->supportedSchemes)) { + $this->response->setStatusCode(400); + return new Authentication\Result( + Authentication\Result::FAILURE_UNCATEGORIZED, + array(), + array('Client requested an incorrect or unsupported authentication scheme') + ); + } + + // client sent a scheme that is not the one required + if (!in_array($clientScheme, $this->acceptSchemes)) { + // challenge again the client + return $this->challengeClient(); + } + + switch ($clientScheme) { + case 'basic': + $result = $this->_basicAuth($authHeader); + break; + case 'digest': + $result = $this->_digestAuth($authHeader); + break; + default: + throw new Exception\RuntimeException('Unsupported authentication scheme: ' . $clientScheme); + } + + return $result; + } + + /** + * @deprecated + * @see Http::challengeClient() + * @return Authentication\Result Always returns a non-identity Auth result + */ + protected function _challengeClient() + { + trigger_error(sprintf( + 'The method "%s" is deprecated and will be removed in the future; ' + . 'please use the public method "%s::challengeClient()" instead', + __METHOD__, + __CLASS__ + ), E_USER_DEPRECATED); + + return $this->challengeClient(); + } + + /** + * Challenge Client + * + * Sets a 401 or 407 Unauthorized response code, and creates the + * appropriate Authenticate header(s) to prompt for credentials. + * + * @return Authentication\Result Always returns a non-identity Auth result + */ + public function challengeClient() + { + if ($this->imaProxy) { + $statusCode = 407; + $headerName = 'Proxy-Authenticate'; + } else { + $statusCode = 401; + $headerName = 'WWW-Authenticate'; + } + + $this->response->setStatusCode($statusCode); + + // Send a challenge in each acceptable authentication scheme + $headers = $this->response->getHeaders(); + if (in_array('basic', $this->acceptSchemes)) { + $headers->addHeaderLine($headerName, $this->_basicHeader()); + } + if (in_array('digest', $this->acceptSchemes)) { + $headers->addHeaderLine($headerName, $this->_digestHeader()); + } + return new Authentication\Result( + Authentication\Result::FAILURE_CREDENTIAL_INVALID, + array(), + array('Invalid or absent credentials; challenging client') + ); + } + + /** + * Basic Header + * + * Generates a Proxy- or WWW-Authenticate header value in the Basic + * authentication scheme. + * + * @return string Authenticate header value + */ + protected function _basicHeader() + { + return 'Basic realm="' . $this->realm . '"'; + } + + /** + * Digest Header + * + * Generates a Proxy- or WWW-Authenticate header value in the Digest + * authentication scheme. + * + * @return string Authenticate header value + */ + protected function _digestHeader() + { + $wwwauth = 'Digest realm="' . $this->realm . '", ' + . 'domain="' . $this->domains . '", ' + . 'nonce="' . $this->_calcNonce() . '", ' + . ($this->useOpaque ? 'opaque="' . $this->_calcOpaque() . '", ' : '') + . 'algorithm="' . $this->algo . '", ' + . 'qop="' . implode(',', $this->supportedQops) . '"'; + + return $wwwauth; + } + + /** + * Basic Authentication + * + * @param string $header Client's Authorization header + * @throws Exception\ExceptionInterface + * @return Authentication\Result + */ + protected function _basicAuth($header) + { + if (empty($header)) { + throw new Exception\RuntimeException('The value of the client Authorization header is required'); + } + if (empty($this->basicResolver)) { + throw new Exception\RuntimeException( + 'A basicResolver object must be set before doing Basic authentication' + ); + } + + // Decode the Authorization header + $auth = substr($header, strlen('Basic ')); + $auth = base64_decode($auth); + if (!$auth) { + throw new Exception\RuntimeException('Unable to base64_decode Authorization header value'); + } + + // See ZF-1253. Validate the credentials the same way the digest + // implementation does. If invalid credentials are detected, + // re-challenge the client. + if (!ctype_print($auth)) { + return $this->challengeClient(); + } + // Fix for ZF-1515: Now re-challenges on empty username or password + $creds = array_filter(explode(':', $auth)); + if (count($creds) != 2) { + return $this->challengeClient(); + } + + $result = $this->basicResolver->resolve($creds[0], $this->realm, $creds[1]); + + if ($result instanceof Authentication\Result && $result->isValid()) { + return $result; + } + + if (!$result instanceof Authentication\Result + && !is_array($result) + && CryptUtils::compareStrings($result, $creds[1]) + ) { + $identity = array('username' => $creds[0], 'realm' => $this->realm); + return new Authentication\Result(Authentication\Result::SUCCESS, $identity); + } elseif (is_array($result)) { + return new Authentication\Result(Authentication\Result::SUCCESS, $result); + } + + return $this->challengeClient(); + } + + /** + * Digest Authentication + * + * @param string $header Client's Authorization header + * @throws Exception\ExceptionInterface + * @return Authentication\Result Valid auth result only on successful auth + */ + protected function _digestAuth($header) + { + if (empty($header)) { + throw new Exception\RuntimeException('The value of the client Authorization header is required'); + } + if (empty($this->digestResolver)) { + throw new Exception\RuntimeException('A digestResolver object must be set before doing Digest authentication'); + } + + $data = $this->_parseDigestAuth($header); + if ($data === false) { + $this->response->setStatusCode(400); + return new Authentication\Result( + Authentication\Result::FAILURE_UNCATEGORIZED, + array(), + array('Invalid Authorization header format') + ); + } + + // See ZF-1052. This code was a bit too unforgiving of invalid + // usernames. Now, if the username is bad, we re-challenge the client. + if ('::invalid::' == $data['username']) { + return $this->challengeClient(); + } + + // Verify that the client sent back the same nonce + if ($this->_calcNonce() != $data['nonce']) { + return $this->challengeClient(); + } + // The opaque value is also required to match, but of course IE doesn't + // play ball. + if (!$this->ieNoOpaque && $this->_calcOpaque() != $data['opaque']) { + return $this->challengeClient(); + } + + // Look up the user's password hash. If not found, deny access. + // This makes no assumptions about how the password hash was + // constructed beyond that it must have been built in such a way as + // to be recreatable with the current settings of this object. + $ha1 = $this->digestResolver->resolve($data['username'], $data['realm']); + if ($ha1 === false) { + return $this->challengeClient(); + } + + // If MD5-sess is used, a1 value is made of the user's password + // hash with the server and client nonce appended, separated by + // colons. + if ($this->algo == 'MD5-sess') { + $ha1 = hash('md5', $ha1 . ':' . $data['nonce'] . ':' . $data['cnonce']); + } + + // Calculate h(a2). The value of this hash depends on the qop + // option selected by the client and the supported hash functions + switch ($data['qop']) { + case 'auth': + $a2 = $this->request->getMethod() . ':' . $data['uri']; + break; + case 'auth-int': + // Should be REQUEST_METHOD . ':' . uri . ':' . hash(entity-body), + // but this isn't supported yet, so fall through to default case + default: + throw new Exception\RuntimeException('Client requested an unsupported qop option'); + } + // Using hash() should make parameterizing the hash algorithm + // easier + $ha2 = hash('md5', $a2); + + // Calculate the server's version of the request-digest. This must + // match $data['response']. See RFC 2617, section 3.2.2.1 + $message = $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' . $ha2; + $digest = hash('md5', $ha1 . ':' . $message); + + // If our digest matches the client's let them in, otherwise return + // a 401 code and exit to prevent access to the protected resource. + if (CryptUtils::compareStrings($digest, $data['response'])) { + $identity = array('username' => $data['username'], 'realm' => $data['realm']); + return new Authentication\Result(Authentication\Result::SUCCESS, $identity); + } + + return $this->challengeClient(); + } + + /** + * Calculate Nonce + * + * @return string The nonce value + */ + protected function _calcNonce() + { + // Once subtle consequence of this timeout calculation is that it + // actually divides all of time into nonceTimeout-sized sections, such + // that the value of timeout is the point in time of the next + // approaching "boundary" of a section. This allows the server to + // consistently generate the same timeout (and hence the same nonce + // value) across requests, but only as long as one of those + // "boundaries" is not crossed between requests. If that happens, the + // nonce will change on its own, and effectively log the user out. This + // would be surprising if the user just logged in. + $timeout = ceil(time() / $this->nonceTimeout) * $this->nonceTimeout; + + $userAgentHeader = $this->request->getHeaders()->get('User-Agent'); + if ($userAgentHeader) { + $userAgent = $userAgentHeader->getFieldValue(); + } elseif (isset($_SERVER['HTTP_USER_AGENT'])) { + $userAgent = $_SERVER['HTTP_USER_AGENT']; + } else { + $userAgent = 'Zend_Authenticaion'; + } + $nonce = hash('md5', $timeout . ':' . $userAgent . ':' . __CLASS__); + return $nonce; + } + + /** + * Calculate Opaque + * + * The opaque string can be anything; the client must return it exactly as + * it was sent. It may be useful to store data in this string in some + * applications. Ideally, a new value for this would be generated each time + * a WWW-Authenticate header is sent (in order to reduce predictability), + * but we would have to be able to create the same exact value across at + * least two separate requests from the same client. + * + * @return string The opaque value + */ + protected function _calcOpaque() + { + return hash('md5', 'Opaque Data:' . __CLASS__); + } + + /** + * Parse Digest Authorization header + * + * @param string $header Client's Authorization: HTTP header + * @return array|bool Data elements from header, or false if any part of + * the header is invalid + */ + protected function _parseDigestAuth($header) + { + $temp = null; + $data = array(); + + // See ZF-1052. Detect invalid usernames instead of just returning a + // 400 code. + $ret = preg_match('/username="([^"]+)"/', $header, $temp); + if (!$ret || empty($temp[1]) + || !ctype_print($temp[1]) + || strpos($temp[1], ':') !== false) { + $data['username'] = '::invalid::'; + } else { + $data['username'] = $temp[1]; + } + $temp = null; + + $ret = preg_match('/realm="([^"]+)"/', $header, $temp); + if (!$ret || empty($temp[1])) { + return false; + } + if (!ctype_print($temp[1]) || strpos($temp[1], ':') !== false) { + return false; + } else { + $data['realm'] = $temp[1]; + } + $temp = null; + + $ret = preg_match('/nonce="([^"]+)"/', $header, $temp); + if (!$ret || empty($temp[1])) { + return false; + } + if (!ctype_xdigit($temp[1])) { + return false; + } + + $data['nonce'] = $temp[1]; + $temp = null; + + $ret = preg_match('/uri="([^"]+)"/', $header, $temp); + if (!$ret || empty($temp[1])) { + return false; + } + // Section 3.2.2.5 in RFC 2617 says the authenticating server must + // verify that the URI field in the Authorization header is for the + // same resource requested in the Request Line. + $rUri = $this->request->getUri(); + $cUri = UriFactory::factory($temp[1]); + + // Make sure the path portion of both URIs is the same + if ($rUri->getPath() != $cUri->getPath()) { + return false; + } + + // Section 3.2.2.5 seems to suggest that the value of the URI + // Authorization field should be made into an absolute URI if the + // Request URI is absolute, but it's vague, and that's a bunch of + // code I don't want to write right now. + $data['uri'] = $temp[1]; + $temp = null; + + $ret = preg_match('/response="([^"]+)"/', $header, $temp); + if (!$ret || empty($temp[1])) { + return false; + } + if (32 != strlen($temp[1]) || !ctype_xdigit($temp[1])) { + return false; + } + + $data['response'] = $temp[1]; + $temp = null; + + // The spec says this should default to MD5 if omitted. OK, so how does + // that square with the algo we send out in the WWW-Authenticate header, + // if it can easily be overridden by the client? + $ret = preg_match('/algorithm="?(' . $this->algo . ')"?/', $header, $temp); + if ($ret && !empty($temp[1]) + && in_array($temp[1], $this->supportedAlgos)) { + $data['algorithm'] = $temp[1]; + } else { + $data['algorithm'] = 'MD5'; // = $this->algo; ? + } + $temp = null; + + // Not optional in this implementation + $ret = preg_match('/cnonce="([^"]+)"/', $header, $temp); + if (!$ret || empty($temp[1])) { + return false; + } + if (!ctype_print($temp[1])) { + return false; + } + + $data['cnonce'] = $temp[1]; + $temp = null; + + // If the server sent an opaque value, the client must send it back + if ($this->useOpaque) { + $ret = preg_match('/opaque="([^"]+)"/', $header, $temp); + if (!$ret || empty($temp[1])) { + // Big surprise: IE isn't RFC 2617-compliant. + $headers = $this->request->getHeaders(); + if (!$headers->has('User-Agent')) { + return false; + } + $userAgent = $headers->get('User-Agent')->getFieldValue(); + if (false === strpos($userAgent, 'MSIE')) { + return false; + } + + $temp[1] = ''; + $this->ieNoOpaque = true; + } + + // This implementation only sends MD5 hex strings in the opaque value + if (!$this->ieNoOpaque && + (32 != strlen($temp[1]) || !ctype_xdigit($temp[1]))) { + return false; + } + + $data['opaque'] = $temp[1]; + $temp = null; + } + + // Not optional in this implementation, but must be one of the supported + // qop types + $ret = preg_match('/qop="?(' . implode('|', $this->supportedQops) . ')"?/', $header, $temp); + if (!$ret || empty($temp[1])) { + return false; + } + if (!in_array($temp[1], $this->supportedQops)) { + return false; + } + + $data['qop'] = $temp[1]; + $temp = null; + + // Not optional in this implementation. The spec says this value + // shouldn't be a quoted string, but apparently some implementations + // quote it anyway. See ZF-1544. + $ret = preg_match('/nc="?([0-9A-Fa-f]{8})"?/', $header, $temp); + if (!$ret || empty($temp[1])) { + return false; + } + if (8 != strlen($temp[1]) || !ctype_xdigit($temp[1])) { + return false; + } + + $data['nc'] = $temp[1]; + $temp = null; + + return $data; + } +} diff --git a/library/Zend/Authentication/Adapter/Http/ApacheResolver.php b/library/Zend/Authentication/Adapter/Http/ApacheResolver.php new file mode 100755 index 0000000000..a7b8b3d6d8 --- /dev/null +++ b/library/Zend/Authentication/Adapter/Http/ApacheResolver.php @@ -0,0 +1,171 @@ +setFile($path); + } + } + + /** + * Set the path to the credentials file + * + * @param string $path + * @return self Provides a fluent interface + * @throws Exception\InvalidArgumentException if path is not readable + */ + public function setFile($path) + { + if (empty($path) || !is_readable($path)) { + throw new Exception\InvalidArgumentException('Path not readable: ' . $path); + } + $this->file = $path; + + return $this; + } + + /** + * Returns the path to the credentials file + * + * @return string + */ + public function getFile() + { + return $this->file; + } + + /** + * Returns the Apache Password object + * + * @return ApachePassword + */ + protected function getApachePassword() + { + if (empty($this->apachePassword)) { + $this->apachePassword = new ApachePassword(); + } + return $this->apachePassword; + } + + /** + * Resolve credentials + * + * + * + * @param string $username Username + * @param string $realm Authentication Realm + * @param string $password The password to authenticate + * @return AuthResult + * @throws Exception\ExceptionInterface + */ + public function resolve($username, $realm, $password = null) + { + if (empty($username)) { + throw new Exception\InvalidArgumentException('Username is required'); + } + + if (!ctype_print($username) || strpos($username, ':') !== false) { + throw new Exception\InvalidArgumentException( + 'Username must consist only of printable characters, excluding the colon' + ); + } + + if (!empty($realm) && (!ctype_print($realm) || strpos($realm, ':') !== false)) { + throw new Exception\InvalidArgumentException( + 'Realm must consist only of printable characters, excluding the colon' + ); + } + + if (empty($password)) { + throw new Exception\InvalidArgumentException('Password is required'); + } + + // Open file, read through looking for matching credentials + ErrorHandler::start(E_WARNING); + $fp = fopen($this->file, 'r'); + $error = ErrorHandler::stop(); + if (!$fp) { + throw new Exception\RuntimeException('Unable to open password file: ' . $this->file, 0, $error); + } + + // No real validation is done on the contents of the password file. The + // assumption is that we trust the administrators to keep it secure. + while (($line = fgetcsv($fp, 512, ':')) !== false) { + if ($line[0] != $username) { + continue; + } + + if (isset($line[2])) { + if ($line[1] == $realm) { + $matchedHash = $line[2]; + break; + } + continue; + } + + $matchedHash = $line[1]; + break; + } + fclose($fp); + + if (!isset($matchedHash)) { + return new AuthResult(AuthResult::FAILURE_IDENTITY_NOT_FOUND, null, array('Username not found in provided htpasswd file')); + } + + // Plaintext password + if ($matchedHash === $password) { + return new AuthResult(AuthResult::SUCCESS, $username); + } + + $apache = $this->getApachePassword(); + $apache->setUserName($username); + if (!empty($realm)) { + $apache->setAuthName($realm); + } + + if ($apache->verify($password, $matchedHash)) { + return new AuthResult(AuthResult::SUCCESS, $username); + } + + return new AuthResult(AuthResult::FAILURE_CREDENTIAL_INVALID, null, array('Passwords did not match.')); + } +} diff --git a/library/Zend/Authentication/Adapter/Http/Exception/ExceptionInterface.php b/library/Zend/Authentication/Adapter/Http/Exception/ExceptionInterface.php new file mode 100755 index 0000000000..4ec6a3f7ca --- /dev/null +++ b/library/Zend/Authentication/Adapter/Http/Exception/ExceptionInterface.php @@ -0,0 +1,19 @@ +setFile($path); + } + } + + /** + * Set the path to the credentials file + * + * @param string $path + * @return FileResolver Provides a fluent interface + * @throws Exception\InvalidArgumentException if path is not readable + */ + public function setFile($path) + { + if (empty($path) || !is_readable($path)) { + throw new Exception\InvalidArgumentException('Path not readable: ' . $path); + } + $this->file = $path; + + return $this; + } + + /** + * Returns the path to the credentials file + * + * @return string + */ + public function getFile() + { + return $this->file; + } + + /** + * Resolve credentials + * + * Only the first matching username/realm combination in the file is + * returned. If the file contains credentials for Digest authentication, + * the returned string is the password hash, or h(a1) from RFC 2617. The + * returned string is the plain-text password for Basic authentication. + * + * The expected format of the file is: + * username:realm:sharedSecret + * + * That is, each line consists of the user's username, the applicable + * authentication realm, and the password or hash, each delimited by + * colons. + * + * @param string $username Username + * @param string $realm Authentication Realm + * @return string|false User's shared secret, if the user is found in the + * realm, false otherwise. + * @throws Exception\ExceptionInterface + */ + public function resolve($username, $realm, $password = null) + { + if (empty($username)) { + throw new Exception\InvalidArgumentException('Username is required'); + } elseif (!ctype_print($username) || strpos($username, ':') !== false) { + throw new Exception\InvalidArgumentException( + 'Username must consist only of printable characters, excluding the colon' + ); + } + if (empty($realm)) { + throw new Exception\InvalidArgumentException('Realm is required'); + } elseif (!ctype_print($realm) || strpos($realm, ':') !== false) { + throw new Exception\InvalidArgumentException( + 'Realm must consist only of printable characters, excluding the colon.' + ); + } + + // Open file, read through looking for matching credentials + ErrorHandler::start(E_WARNING); + $fp = fopen($this->file, 'r'); + $error = ErrorHandler::stop(); + if (!$fp) { + throw new Exception\RuntimeException('Unable to open password file: ' . $this->file, 0, $error); + } + + // No real validation is done on the contents of the password file. The + // assumption is that we trust the administrators to keep it secure. + while (($line = fgetcsv($fp, 512, ':')) !== false) { + if ($line[0] == $username && $line[1] == $realm) { + $password = $line[2]; + fclose($fp); + return $password; + } + } + + fclose($fp); + return false; + } +} diff --git a/library/Zend/Authentication/Adapter/Http/ResolverInterface.php b/library/Zend/Authentication/Adapter/Http/ResolverInterface.php new file mode 100755 index 0000000000..b236f84588 --- /dev/null +++ b/library/Zend/Authentication/Adapter/Http/ResolverInterface.php @@ -0,0 +1,30 @@ +setOptions($options); + if ($identity !== null) { + $this->setIdentity($identity); + } + if ($credential !== null) { + $this->setCredential($credential); + } + } + + /** + * Returns the array of arrays of Zend\Ldap\Ldap options of this adapter. + * + * @return array|null + */ + public function getOptions() + { + return $this->options; + } + + /** + * Sets the array of arrays of Zend\Ldap\Ldap options to be used by + * this adapter. + * + * @param array $options The array of arrays of Zend\Ldap\Ldap options + * @return Ldap Provides a fluent interface + */ + public function setOptions($options) + { + $this->options = is_array($options) ? $options : array(); + if (array_key_exists('identity', $this->options)) { + $this->options['username'] = $this->options['identity']; + } + if (array_key_exists('credential', $this->options)) { + $this->options['password'] = $this->options['credential']; + } + return $this; + } + + /** + * Returns the username of the account being authenticated, or + * NULL if none is set. + * + * @return string|null + */ + public function getUsername() + { + return $this->getIdentity(); + } + + /** + * Sets the username for binding + * + * @param string $username The username for binding + * @return Ldap Provides a fluent interface + */ + public function setUsername($username) + { + return $this->setIdentity($username); + } + + /** + * Returns the password of the account being authenticated, or + * NULL if none is set. + * + * @return string|null + */ + public function getPassword() + { + return $this->getCredential(); + } + + /** + * Sets the password for the account + * + * @param string $password The password of the account being authenticated + * @return Ldap Provides a fluent interface + */ + public function setPassword($password) + { + return $this->setCredential($password); + } + + /** + * Returns the LDAP Object + * + * @return ZendLdap\Ldap The Zend\Ldap\Ldap object used to authenticate the credentials + */ + public function getLdap() + { + if ($this->ldap === null) { + $this->ldap = new ZendLdap\Ldap(); + } + + return $this->ldap; + } + + /** + * Set an Ldap connection + * + * @param ZendLdap\Ldap $ldap An existing Ldap object + * @return Ldap Provides a fluent interface + */ + public function setLdap(ZendLdap\Ldap $ldap) + { + $this->ldap = $ldap; + + $this->setOptions(array($ldap->getOptions())); + + return $this; + } + + /** + * Returns a domain name for the current LDAP options. This is used + * for skipping redundant operations (e.g. authentications). + * + * @return string + */ + protected function getAuthorityName() + { + $options = $this->getLdap()->getOptions(); + $name = $options['accountDomainName']; + if (!$name) { + $name = $options['accountDomainNameShort']; + } + + return $name ? $name : ''; + } + + /** + * Authenticate the user + * + * @return AuthenticationResult + * @throws Exception\ExceptionInterface + */ + public function authenticate() + { + $messages = array(); + $messages[0] = ''; // reserved + $messages[1] = ''; // reserved + + $username = $this->identity; + $password = $this->credential; + + if (!$username) { + $code = AuthenticationResult::FAILURE_IDENTITY_NOT_FOUND; + $messages[0] = 'A username is required'; + return new AuthenticationResult($code, '', $messages); + } + if (!$password) { + /* A password is required because some servers will + * treat an empty password as an anonymous bind. + */ + $code = AuthenticationResult::FAILURE_CREDENTIAL_INVALID; + $messages[0] = 'A password is required'; + return new AuthenticationResult($code, '', $messages); + } + + $ldap = $this->getLdap(); + + $code = AuthenticationResult::FAILURE; + $messages[0] = "Authority not found: $username"; + $failedAuthorities = array(); + + /* Iterate through each server and try to authenticate the supplied + * credentials against it. + */ + foreach ($this->options as $options) { + if (!is_array($options)) { + throw new Exception\InvalidArgumentException('Adapter options array not an array'); + } + $adapterOptions = $this->prepareOptions($ldap, $options); + $dname = ''; + + try { + if ($messages[1]) { + $messages[] = $messages[1]; + } + + $messages[1] = ''; + $messages[] = $this->optionsToString($options); + + $dname = $this->getAuthorityName(); + if (isset($failedAuthorities[$dname])) { + /* If multiple sets of server options for the same domain + * are supplied, we want to skip redundant authentications + * where the identity or credentials where found to be + * invalid with another server for the same domain. The + * $failedAuthorities array tracks this condition (and also + * serves to supply the original error message). + * This fixes issue ZF-4093. + */ + $messages[1] = $failedAuthorities[$dname]; + $messages[] = "Skipping previously failed authority: $dname"; + continue; + } + + $canonicalName = $ldap->getCanonicalAccountName($username); + $ldap->bind($canonicalName, $password); + /* + * Fixes problem when authenticated user is not allowed to retrieve + * group-membership information or own account. + * This requires that the user specified with "username" and optionally + * "password" in the Zend\Ldap\Ldap options is able to retrieve the required + * information. + */ + $requireRebind = false; + if (isset($options['username'])) { + $ldap->bind(); + $requireRebind = true; + } + $dn = $ldap->getCanonicalAccountName($canonicalName, ZendLdap\Ldap::ACCTNAME_FORM_DN); + + $groupResult = $this->checkGroupMembership($ldap, $canonicalName, $dn, $adapterOptions); + if ($groupResult === true) { + $this->authenticatedDn = $dn; + $messages[0] = ''; + $messages[1] = ''; + $messages[] = "$canonicalName authentication successful"; + if ($requireRebind === true) { + // rebinding with authenticated user + $ldap->bind($dn, $password); + } + return new AuthenticationResult(AuthenticationResult::SUCCESS, $canonicalName, $messages); + } else { + $messages[0] = 'Account is not a member of the specified group'; + $messages[1] = $groupResult; + $failedAuthorities[$dname] = $groupResult; + } + } catch (LdapException $zle) { + /* LDAP based authentication is notoriously difficult to diagnose. Therefore + * we bend over backwards to capture and record every possible bit of + * information when something goes wrong. + */ + + $err = $zle->getCode(); + + if ($err == LdapException::LDAP_X_DOMAIN_MISMATCH) { + /* This error indicates that the domain supplied in the + * username did not match the domains in the server options + * and therefore we should just skip to the next set of + * server options. + */ + continue; + } elseif ($err == LdapException::LDAP_NO_SUCH_OBJECT) { + $code = AuthenticationResult::FAILURE_IDENTITY_NOT_FOUND; + $messages[0] = "Account not found: $username"; + $failedAuthorities[$dname] = $zle->getMessage(); + } elseif ($err == LdapException::LDAP_INVALID_CREDENTIALS) { + $code = AuthenticationResult::FAILURE_CREDENTIAL_INVALID; + $messages[0] = 'Invalid credentials'; + $failedAuthorities[$dname] = $zle->getMessage(); + } else { + $line = $zle->getLine(); + $messages[] = $zle->getFile() . "($line): " . $zle->getMessage(); + $messages[] = preg_replace( + '/\b'.preg_quote(substr($password, 0, 15), '/').'\b/', + '*****', + $zle->getTraceAsString() + ); + $messages[0] = 'An unexpected failure occurred'; + } + $messages[1] = $zle->getMessage(); + } + } + + $msg = isset($messages[1]) ? $messages[1] : $messages[0]; + $messages[] = "$username authentication failed: $msg"; + + return new AuthenticationResult($code, $username, $messages); + } + + /** + * Sets the LDAP specific options on the Zend\Ldap\Ldap instance + * + * @param ZendLdap\Ldap $ldap + * @param array $options + * @return array of auth-adapter specific options + */ + protected function prepareOptions(ZendLdap\Ldap $ldap, array $options) + { + $adapterOptions = array( + 'group' => null, + 'groupDn' => $ldap->getBaseDn(), + 'groupScope' => ZendLdap\Ldap::SEARCH_SCOPE_SUB, + 'groupAttr' => 'cn', + 'groupFilter' => 'objectClass=groupOfUniqueNames', + 'memberAttr' => 'uniqueMember', + 'memberIsDn' => true + ); + foreach ($adapterOptions as $key => $value) { + if (array_key_exists($key, $options)) { + $value = $options[$key]; + unset($options[$key]); + switch ($key) { + case 'groupScope': + $value = (int) $value; + if (in_array( + $value, + array( + ZendLdap\Ldap::SEARCH_SCOPE_BASE, + ZendLdap\Ldap::SEARCH_SCOPE_ONE, + ZendLdap\Ldap::SEARCH_SCOPE_SUB, + ), + true + )) { + $adapterOptions[$key] = $value; + } + break; + case 'memberIsDn': + $adapterOptions[$key] = ($value === true || + $value === '1' || strcasecmp($value, 'true') == 0); + break; + default: + $adapterOptions[$key] = trim($value); + break; + } + } + } + $ldap->setOptions($options); + return $adapterOptions; + } + + /** + * Checks the group membership of the bound user + * + * @param ZendLdap\Ldap $ldap + * @param string $canonicalName + * @param string $dn + * @param array $adapterOptions + * @return string|true + */ + protected function checkGroupMembership(ZendLdap\Ldap $ldap, $canonicalName, $dn, array $adapterOptions) + { + if ($adapterOptions['group'] === null) { + return true; + } + + if ($adapterOptions['memberIsDn'] === false) { + $user = $canonicalName; + } else { + $user = $dn; + } + + $groupName = ZendLdap\Filter::equals($adapterOptions['groupAttr'], $adapterOptions['group']); + $membership = ZendLdap\Filter::equals($adapterOptions['memberAttr'], $user); + $group = ZendLdap\Filter::andFilter($groupName, $membership); + $groupFilter = $adapterOptions['groupFilter']; + if (!empty($groupFilter)) { + $group = $group->addAnd($groupFilter); + } + + $result = $ldap->count($group, $adapterOptions['groupDn'], $adapterOptions['groupScope']); + + if ($result === 1) { + return true; + } + + return 'Failed to verify group membership with ' . $group->toString(); + } + + /** + * getAccountObject() - Returns the result entry as a stdClass object + * + * This resembles the feature {@see Zend\Authentication\Adapter\DbTable::getResultRowObject()}. + * Closes ZF-6813 + * + * @param array $returnAttribs + * @param array $omitAttribs + * @return stdClass|bool + */ + public function getAccountObject(array $returnAttribs = array(), array $omitAttribs = array()) + { + if (!$this->authenticatedDn) { + return false; + } + + $returnObject = new stdClass(); + + $returnAttribs = array_map('strtolower', $returnAttribs); + $omitAttribs = array_map('strtolower', $omitAttribs); + $returnAttribs = array_diff($returnAttribs, $omitAttribs); + + $entry = $this->getLdap()->getEntry($this->authenticatedDn, $returnAttribs, true); + foreach ($entry as $attr => $value) { + if (in_array($attr, $omitAttribs)) { + // skip attributes marked to be omitted + continue; + } + if (is_array($value)) { + $returnObject->$attr = (count($value) > 1) ? $value : $value[0]; + } else { + $returnObject->$attr = $value; + } + } + return $returnObject; + } + + /** + * Converts options to string + * + * @param array $options + * @return string + */ + private function optionsToString(array $options) + { + $str = ''; + foreach ($options as $key => $val) { + if ($key === 'password' || $key === 'credential') { + $val = '*****'; + } + if ($str) { + $str .= ','; + } + $str .= $key . '=' . $val; + } + return $str; + } +} diff --git a/library/Zend/Authentication/Adapter/ValidatableAdapterInterface.php b/library/Zend/Authentication/Adapter/ValidatableAdapterInterface.php new file mode 100755 index 0000000000..3c4f01b2c7 --- /dev/null +++ b/library/Zend/Authentication/Adapter/ValidatableAdapterInterface.php @@ -0,0 +1,45 @@ +setStorage($storage); + } + if (null !== $adapter) { + $this->setAdapter($adapter); + } + } + + /** + * Returns the authentication adapter + * + * The adapter does not have a default if the storage adapter has not been set. + * + * @return Adapter\AdapterInterface|null + */ + public function getAdapter() + { + return $this->adapter; + } + + /** + * Sets the authentication adapter + * + * @param Adapter\AdapterInterface $adapter + * @return AuthenticationService Provides a fluent interface + */ + public function setAdapter(Adapter\AdapterInterface $adapter) + { + $this->adapter = $adapter; + return $this; + } + + /** + * Returns the persistent storage handler + * + * Session storage is used by default unless a different storage adapter has been set. + * + * @return Storage\StorageInterface + */ + public function getStorage() + { + if (null === $this->storage) { + $this->setStorage(new Storage\Session()); + } + + return $this->storage; + } + + /** + * Sets the persistent storage handler + * + * @param Storage\StorageInterface $storage + * @return AuthenticationService Provides a fluent interface + */ + public function setStorage(Storage\StorageInterface $storage) + { + $this->storage = $storage; + return $this; + } + + /** + * Authenticates against the supplied adapter + * + * @param Adapter\AdapterInterface $adapter + * @return Result + * @throws Exception\RuntimeException + */ + public function authenticate(Adapter\AdapterInterface $adapter = null) + { + if (!$adapter) { + if (!$adapter = $this->getAdapter()) { + throw new Exception\RuntimeException('An adapter must be set or passed prior to calling authenticate()'); + } + } + $result = $adapter->authenticate(); + + /** + * ZF-7546 - prevent multiple successive calls from storing inconsistent results + * Ensure storage has clean state + */ + if ($this->hasIdentity()) { + $this->clearIdentity(); + } + + if ($result->isValid()) { + $this->getStorage()->write($result->getIdentity()); + } + + return $result; + } + + /** + * Returns true if and only if an identity is available from storage + * + * @return bool + */ + public function hasIdentity() + { + return !$this->getStorage()->isEmpty(); + } + + /** + * Returns the identity from storage or null if no identity is available + * + * @return mixed|null + */ + public function getIdentity() + { + $storage = $this->getStorage(); + + if ($storage->isEmpty()) { + return null; + } + + return $storage->read(); + } + + /** + * Clears the identity from persistent storage + * + * @return void + */ + public function clearIdentity() + { + $this->getStorage()->clear(); + } +} diff --git a/library/Zend/Authentication/AuthenticationServiceInterface.php b/library/Zend/Authentication/AuthenticationServiceInterface.php new file mode 100755 index 0000000000..fcf74ea17e --- /dev/null +++ b/library/Zend/Authentication/AuthenticationServiceInterface.php @@ -0,0 +1,44 @@ +code = (int) $code; + $this->identity = $identity; + $this->messages = $messages; + } + + /** + * Returns whether the result represents a successful authentication attempt + * + * @return bool + */ + public function isValid() + { + return ($this->code > 0) ? true : false; + } + + /** + * getCode() - Get the result code for this authentication attempt + * + * @return int + */ + public function getCode() + { + return $this->code; + } + + /** + * Returns the identity used in the authentication attempt + * + * @return mixed + */ + public function getIdentity() + { + return $this->identity; + } + + /** + * Returns an array of string reasons why the authentication attempt was unsuccessful + * + * If authentication was successful, this method returns an empty array. + * + * @return array + */ + public function getMessages() + { + return $this->messages; + } +} diff --git a/library/Zend/Authentication/Storage/Chain.php b/library/Zend/Authentication/Storage/Chain.php new file mode 100755 index 0000000000..8d995a2c0a --- /dev/null +++ b/library/Zend/Authentication/Storage/Chain.php @@ -0,0 +1,109 @@ +storageChain = new PriorityQueue(); + } + + /** + * @param StorageInterface $storage + * @param int $priority + */ + public function add(StorageInterface $storage, $priority = 1) + { + $this->storageChain->insert($storage, $priority); + } + + /** + * Loop over the queue of storage until a storage is found that is non-empty. If such + * storage is not found, then this chain storage itself is empty. + * + * In case a non-empty storage is found then this chain storage is also non-empty. Report + * that, but also make sure that all storage with higher priorty that are empty + * are filled. + * + * @see StorageInterface::isEmpty() + */ + public function isEmpty() + { + $storageWithHigherPriority = array(); + + // Loop invariant: $storageWithHigherPriority contains all storage with higher priorty + // than the current one. + foreach ($this->storageChain as $storage) { + if ($storage->isEmpty()) { + $storageWithHigherPriority[] = $storage; + continue; + } + + $storageValue = $storage->read(); + foreach ($storageWithHigherPriority as $higherPriorityStorage) { + $higherPriorityStorage->write($storageValue); + } + + return false; + } + + return true; + } + + /** + * If the chain is non-empty then the storage with the top priority is guaranteed to be + * filled. Return its value. + * + * @see StorageInterface::read() + */ + public function read() + { + return $this->storageChain->top()->read(); + } + + /** + * Write the new $contents to all storage in the chain. + * + * @see StorageInterface::write() + */ + public function write($contents) + { + foreach ($this->storageChain as $storage) { + $storage->write($contents); + } + } + + /** + * Clear all storage in the chain. + * + * @see StorageInterface::clear() + */ + public function clear() + { + foreach ($this->storageChain as $storage) { + $storage->clear(); + } + } +} diff --git a/library/Zend/Authentication/Storage/NonPersistent.php b/library/Zend/Authentication/Storage/NonPersistent.php new file mode 100755 index 0000000000..8d12049390 --- /dev/null +++ b/library/Zend/Authentication/Storage/NonPersistent.php @@ -0,0 +1,67 @@ +data); + } + + /** + * Returns the contents of storage + * Behavior is undefined when storage is empty. + * + * @return mixed + */ + public function read() + { + return $this->data; + } + + /** + * Writes $contents to storage + * + * @param mixed $contents + * @return void + */ + public function write($contents) + { + $this->data = $contents; + } + + /** + * Clears contents from storage + * + * @return void + */ + public function clear() + { + $this->data = null; + } +} diff --git a/library/Zend/Authentication/Storage/Session.php b/library/Zend/Authentication/Storage/Session.php new file mode 100755 index 0000000000..6c66bec8f6 --- /dev/null +++ b/library/Zend/Authentication/Storage/Session.php @@ -0,0 +1,126 @@ +namespace = $namespace; + } + if ($member !== null) { + $this->member = $member; + } + $this->session = new SessionContainer($this->namespace, $manager); + } + + /** + * Returns the session namespace + * + * @return string + */ + public function getNamespace() + { + return $this->namespace; + } + + /** + * Returns the name of the session object member + * + * @return string + */ + public function getMember() + { + return $this->member; + } + + /** + * Defined by Zend\Authentication\Storage\StorageInterface + * + * @return bool + */ + public function isEmpty() + { + return !isset($this->session->{$this->member}); + } + + /** + * Defined by Zend\Authentication\Storage\StorageInterface + * + * @return mixed + */ + public function read() + { + return $this->session->{$this->member}; + } + + /** + * Defined by Zend\Authentication\Storage\StorageInterface + * + * @param mixed $contents + * @return void + */ + public function write($contents) + { + $this->session->{$this->member} = $contents; + } + + /** + * Defined by Zend\Authentication\Storage\StorageInterface + * + * @return void + */ + public function clear() + { + unset($this->session->{$this->member}); + } +} diff --git a/library/Zend/Authentication/Storage/StorageInterface.php b/library/Zend/Authentication/Storage/StorageInterface.php new file mode 100755 index 0000000000..4a9d41b9e6 --- /dev/null +++ b/library/Zend/Authentication/Storage/StorageInterface.php @@ -0,0 +1,48 @@ + 'Invalid identity', + self::IDENTITY_AMBIGUOUS => 'Identity is ambiguous', + self::CREDENTIAL_INVALID => 'Invalid password', + self::UNCATEGORIZED => 'Authentication failed', + self::GENERAL => 'Authentication failed', + ); + + /** + * Authentication Adapter + * @var ValidatableAdapterInterface + */ + protected $adapter; + + /** + * Identity (or field) + * @var string + */ + protected $identity; + + /** + * Credential (or field) + * @var string + */ + protected $credential; + + /** + * Authentication Service + * @var AuthenticationService + */ + protected $service; + + /** + * Sets validator options + * + * @param mixed $options + */ + public function __construct($options = null) + { + if ($options instanceof Traversable) { + $options = ArrayUtils::iteratorToArray($options); + } + + if (is_array($options)) { + if (array_key_exists('adapter', $options)) { + $this->setAdapter($options['adapter']); + } + if (array_key_exists('identity', $options)) { + $this->setIdentity($options['identity']); + } + if (array_key_exists('credential', $options)) { + $this->setCredential($options['credential']); + } + if (array_key_exists('service', $options)) { + $this->setService($options['service']); + } + } + parent::__construct($options); + } + + /** + * Get Adapter + * + * @return ValidatableAdapterInterface + */ + public function getAdapter() + { + return $this->adapter; + } + + /** + * Set Adapter + * + * @param ValidatableAdapterInterface $adapter + * @return Authentication + */ + public function setAdapter(ValidatableAdapterInterface $adapter) + { + $this->adapter = $adapter; + + return $this; + } + + /** + * Get Identity + * + * @return mixed + */ + public function getIdentity() + { + return $this->identity; + } + + /** + * Set Identity + * + * @param mixed $identity + * @return Authentication + */ + public function setIdentity($identity) + { + $this->identity = $identity; + + return $this; + } + + /** + * Get Credential + * + * @return mixed + */ + public function getCredential() + { + return $this->credential; + } + + /** + * Set Credential + * + * @param mixed $credential + * @return Authentication + */ + public function setCredential($credential) + { + $this->credential = $credential; + + return $this; + } + + /** + * Get Service + * + * @return AuthenticationService + */ + public function getService() + { + return $this->service; + } + + /** + * Set Service + * + * @param AuthenticationService $service + * @return Authentication + */ + public function setService(AuthenticationService $service) + { + $this->service = $service; + + return $this; + } + + /** + * Is Valid + * + * @param mixed $value + * @param array $context + * @return bool + */ + public function isValid($value = null, $context = null) + { + if ($value !== null) { + $this->setCredential($value); + } + + if (($context !== null) && array_key_exists($this->identity, $context)) { + $identity = $context[$this->identity]; + } else { + $identity = $this->identity; + } + if (!$this->identity) { + throw new Exception\RuntimeException('Identity must be set prior to validation'); + } + + if (($context !== null) && array_key_exists($this->credential, $context)) { + $credential = $context[$this->credential]; + } else { + $credential = $this->credential; + } + + if (!$this->adapter) { + throw new Exception\RuntimeException('Adapter must be set prior to validation'); + } + $this->adapter->setIdentity($identity); + $this->adapter->setCredential($credential); + + if (!$this->service) { + throw new Exception\RuntimeException('AuthenticationService must be set prior to validation'); + } + $result = $this->service->authenticate($this->adapter); + + if ($result->getCode() != Result::SUCCESS) { + switch ($result->getCode()) { + case Result::FAILURE_IDENTITY_NOT_FOUND: + $this->error(self::IDENTITY_NOT_FOUND); + break; + case Result::FAILURE_CREDENTIAL_INVALID: + $this->error(self::CREDENTIAL_INVALID); + break; + case Result::FAILURE_IDENTITY_AMBIGUOUS: + $this->error(self::IDENTITY_AMBIGUOUS); + break; + case Result::FAILURE_UNCATEGORIZED: + $this->error(self::UNCATEGORIZED); + break; + default: + $this->error(self::GENERAL); + } + + return false; + } + + return true; + } +} diff --git a/library/Zend/Authentication/composer.json b/library/Zend/Authentication/composer.json new file mode 100755 index 0000000000..fd6d242b99 --- /dev/null +++ b/library/Zend/Authentication/composer.json @@ -0,0 +1,44 @@ +{ + "name": "zendframework/zend-authentication", + "description": "provides an API for authentication and includes concrete authentication adapters for common use case scenarios", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "authentication" + ], + "homepage": "https://github.com/zendframework/zf2", + "autoload": { + "psr-0": { + "Zend\\Authentication\\": "" + } + }, + "target-dir": "Zend/Authentication", + "require": { + "php": ">=5.3.23", + "zendframework/zend-stdlib": "self.version" + }, + "require-dev": { + "zendframework/zend-db": "self.version", + "zendframework/zend-crypt": "self.version", + "zendframework/zend-http": "self.version", + "zendframework/zend-ldap": "self.version", + "zendframework/zend-session": "self.version", + "zendframework/zend-validator": "self.version", + "zendframework/zend-uri": "self.version" + }, + "suggest": { + "zendframework/zend-db": "Zend\\Db component", + "zendframework/zend-crypt": "Zend\\Crypt component", + "zendframework/zend-http": "Zend\\Http component", + "zendframework/zend-ldap": "Zend\\Ldap component", + "zendframework/zend-session": "Zend\\Session component", + "zendframework/zend-uri": "Zend\\Uri component", + "zendframework/zend-validator": "Zend\\Validator component" + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev", + "dev-develop": "2.4-dev" + } + } +} diff --git a/library/Zend/Barcode/Barcode.php b/library/Zend/Barcode/Barcode.php new file mode 100755 index 0000000000..b2176b478a --- /dev/null +++ b/library/Zend/Barcode/Barcode.php @@ -0,0 +1,304 @@ + $e->getMessage())); + $renderer = static::makeRenderer($renderer, array()); + } else { + throw $e; + } + } + + $renderer->setAutomaticRenderError($automaticRenderError); + return $renderer->setBarcode($barcode); + } + + /** + * Barcode Constructor + * + * @param mixed $barcode String name of barcode class, or Traversable object, or barcode object. + * @param mixed $barcodeConfig OPTIONAL; an array or Traversable object with barcode parameters. + * @throws Exception\InvalidArgumentException + * @return Object + */ + public static function makeBarcode($barcode, $barcodeConfig = array()) + { + if ($barcode instanceof Object\ObjectInterface) { + return $barcode; + } + + /* + * Convert Traversable argument to plain string + * barcode name and separate configuration. + */ + if ($barcode instanceof Traversable) { + $barcode = ArrayUtils::iteratorToArray($barcode); + if (isset($barcode['barcodeParams']) && is_array($barcode['barcodeParams'])) { + $barcodeConfig = $barcode['barcodeParams']; + } + if (isset($barcode['barcode'])) { + $barcode = (string) $barcode['barcode']; + } else { + $barcode = null; + } + } + if ($barcodeConfig instanceof Traversable) { + $barcodeConfig = ArrayUtils::iteratorToArray($barcodeConfig); + } + + /* + * Verify that barcode parameters are in an array. + */ + if (!is_array($barcodeConfig)) { + throw new Exception\InvalidArgumentException( + 'Barcode parameters must be in an array or a Traversable object' + ); + } + + /* + * Verify that a barcode name has been specified. + */ + if (!is_string($barcode) || empty($barcode)) { + throw new Exception\InvalidArgumentException( + 'Barcode name must be specified in a string' + ); + } + + return static::getObjectPluginManager()->get($barcode, $barcodeConfig); + } + + /** + * Renderer Constructor + * + * @param mixed $renderer String name of renderer class, or Traversable object. + * @param mixed $rendererConfig OPTIONAL; an array or Traversable object with renderer parameters. + * @throws Exception\RendererCreationException + * @return Renderer\RendererInterface + */ + public static function makeRenderer($renderer = 'image', $rendererConfig = array()) + { + if ($renderer instanceof Renderer\RendererInterface) { + return $renderer; + } + + /* + * Convert Traversable argument to plain string + * barcode name and separate config object. + */ + if ($renderer instanceof Traversable) { + $renderer = ArrayUtils::iteratorToArray($renderer); + if (isset($renderer['rendererParams'])) { + $rendererConfig = $renderer['rendererParams']; + } + if (isset($renderer['renderer'])) { + $renderer = (string) $renderer['renderer']; + } + } + if ($rendererConfig instanceof Traversable) { + $rendererConfig = ArrayUtils::iteratorToArray($rendererConfig); + } + + /* + * Verify that barcode parameters are in an array. + */ + if (!is_array($rendererConfig)) { + throw new Exception\RendererCreationException( + 'Barcode parameters must be in an array or a Traversable object' + ); + } + + /* + * Verify that a barcode name has been specified. + */ + if (!is_string($renderer) || empty($renderer)) { + throw new Exception\RendererCreationException( + 'Renderer name must be specified in a string' + ); + } + + return static::getRendererPluginManager()->get($renderer, $rendererConfig); + } + + /** + * Proxy to renderer render() method + * + * @param string | Object\ObjectInterface | array | Traversable $barcode + * @param string | Renderer\RendererInterface $renderer + * @param array | Traversable $barcodeConfig + * @param array | Traversable $rendererConfig + */ + public static function render( + $barcode, + $renderer, + $barcodeConfig = array(), + $rendererConfig = array() + ) { + static::factory($barcode, $renderer, $barcodeConfig, $rendererConfig)->render(); + } + + /** + * Proxy to renderer draw() method + * + * @param string | Object\ObjectInterface | array | Traversable $barcode + * @param string | Renderer\RendererInterface $renderer + * @param array | Traversable $barcodeConfig + * @param array | Traversable $rendererConfig + * @return mixed + */ + public static function draw( + $barcode, + $renderer, + $barcodeConfig = array(), + $rendererConfig = array() + ) { + return static::factory($barcode, $renderer, $barcodeConfig, $rendererConfig)->draw(); + } + + /** + * Set the default font for new instances of barcode + * + * @param string $font + * @return void + */ + public static function setBarcodeFont($font) + { + static::$staticFont = $font; + } + + /** + * Get current default font + * + * @return string + */ + public static function getBarcodeFont() + { + return static::$staticFont; + } +} diff --git a/library/Zend/Barcode/CONTRIBUTING.md b/library/Zend/Barcode/CONTRIBUTING.md new file mode 100755 index 0000000000..e77f5d2d5b --- /dev/null +++ b/library/Zend/Barcode/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# CONTRIBUTING + +Please don't open pull requests against this repository, please use https://github.com/zendframework/zf2. \ No newline at end of file diff --git a/library/Zend/Barcode/Exception/ExceptionInterface.php b/library/Zend/Barcode/Exception/ExceptionInterface.php new file mode 100755 index 0000000000..f421ceb947 --- /dev/null +++ b/library/Zend/Barcode/Exception/ExceptionInterface.php @@ -0,0 +1,17 @@ +getDefaultOptions(); + $this->font = Barcode\Barcode::getBarcodeFont(); + if ($options instanceof Traversable) { + $options = ArrayUtils::iteratorToArray($options); + } + if (is_array($options)) { + $this->setOptions($options); + } + $this->type = strtolower(substr(get_class($this), strlen($this->barcodeNamespace) + 1)); + if ($this->mandatoryChecksum) { + $this->withChecksum = true; + $this->withChecksumInText = true; + } + } + + /** + * Set default options for particular object + * @return void + */ + protected function getDefaultOptions() + { + } + + /** + * Set barcode state from options array + * @param array $options + * @return \Zend\Barcode\Object\ObjectInterface + */ + public function setOptions($options) + { + foreach ($options as $key => $value) { + $method = 'set' . $key; + if (method_exists($this, $method)) { + $this->$method($value); + } + } + return $this; + } + + /** + * Set barcode namespace for autoloading + * + * @param string $namespace + * @return \Zend\Barcode\Object\ObjectInterface + */ + public function setBarcodeNamespace($namespace) + { + $this->barcodeNamespace = $namespace; + return $this; + } + + /** + * Retrieve barcode namespace + * + * @return string + */ + public function getBarcodeNamespace() + { + return $this->barcodeNamespace; + } + + /** + * Retrieve type of barcode + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Set height of the barcode bar + * @param int $value + * @return \Zend\Barcode\Object\ObjectInterface + * @throws \Zend\Barcode\Object\Exception\ExceptionInterface + */ + public function setBarHeight($value) + { + if (intval($value) <= 0) { + throw new Exception\OutOfRangeException( + 'Bar height must be greater than 0' + ); + } + $this->barHeight = intval($value); + return $this; + } + + /** + * Get height of the barcode bar + * @return int + */ + public function getBarHeight() + { + return $this->barHeight; + } + + /** + * Set thickness of thin bar + * @param int $value + * @return \Zend\Barcode\Object\ObjectInterface + * @throws \Zend\Barcode\Object\Exception\ExceptionInterface + */ + public function setBarThinWidth($value) + { + if (intval($value) <= 0) { + throw new Exception\OutOfRangeException( + 'Bar width must be greater than 0' + ); + } + $this->barThinWidth = intval($value); + return $this; + } + + /** + * Get thickness of thin bar + * @return int + */ + public function getBarThinWidth() + { + return $this->barThinWidth; + } + + /** + * Set thickness of thick bar + * @param int $value + * @return \Zend\Barcode\Object\ObjectInterface + * @throws \Zend\Barcode\Object\Exception\ExceptionInterface + */ + public function setBarThickWidth($value) + { + if (intval($value) <= 0) { + throw new Exception\OutOfRangeException( + 'Bar width must be greater than 0' + ); + } + $this->barThickWidth = intval($value); + return $this; + } + + /** + * Get thickness of thick bar + * @return int + */ + public function getBarThickWidth() + { + return $this->barThickWidth; + } + + /** + * Set factor applying to + * thinBarWidth - thickBarWidth - barHeight - fontSize + * @param float $value + * @return \Zend\Barcode\Object\ObjectInterface + * @throws \Zend\Barcode\Object\Exception\ExceptionInterface + */ + public function setFactor($value) + { + if (floatval($value) <= 0) { + throw new Exception\OutOfRangeException( + 'Factor must be greater than 0' + ); + } + $this->factor = floatval($value); + return $this; + } + + /** + * Get factor applying to + * thinBarWidth - thickBarWidth - barHeight - fontSize + * @return int + */ + public function getFactor() + { + return $this->factor; + } + + /** + * Set color of the barcode and text + * @param string $value + * @return \Zend\Barcode\Object\ObjectInterface + * @throws \Zend\Barcode\Object\Exception\ExceptionInterface + */ + public function setForeColor($value) + { + if (preg_match('`\#[0-9A-F]{6}`', $value)) { + $this->foreColor = hexdec($value); + } elseif (is_numeric($value) && $value >= 0 && $value <= 16777125) { + $this->foreColor = intval($value); + } else { + throw new Exception\InvalidArgumentException( + 'Text color must be set as #[0-9A-F]{6}' + ); + } + return $this; + } + + /** + * Retrieve color of the barcode and text + * @return int + */ + public function getForeColor() + { + return $this->foreColor; + } + + /** + * Set the color of the background + * @param int $value + * @return \Zend\Barcode\Object\ObjectInterface + * @throws \Zend\Barcode\Object\Exception\ExceptionInterface + */ + public function setBackgroundColor($value) + { + if (preg_match('`\#[0-9A-F]{6}`', $value)) { + $this->backgroundColor = hexdec($value); + } elseif (is_numeric($value) && $value >= 0 && $value <= 16777125) { + $this->backgroundColor = intval($value); + } else { + throw new Exception\InvalidArgumentException( + 'Background color must be set as #[0-9A-F]{6}' + ); + } + return $this; + } + + /** + * Retrieve background color of the image + * @return int + */ + public function getBackgroundColor() + { + return $this->backgroundColor; + } + + /** + * Activate/deactivate drawing of the bar + * @param bool $value + * @return \Zend\Barcode\Object\ObjectInterface + */ + public function setWithBorder($value) + { + $this->withBorder = (bool) $value; + return $this; + } + + /** + * Retrieve if border are draw or not + * @return bool + */ + public function getWithBorder() + { + return $this->withBorder; + } + + /** + * Activate/deactivate drawing of the quiet zones + * @param bool $value + * @return AbstractObject + */ + public function setWithQuietZones($value) + { + $this->withQuietZones = (bool) $value; + return $this; + } + + /** + * Retrieve if quiet zones are draw or not + * @return bool + */ + public function getWithQuietZones() + { + return $this->withQuietZones; + } + + /** + * Allow fast inversion of font/bars color and background color + * @return \Zend\Barcode\Object\ObjectInterface + */ + public function setReverseColor() + { + $tmp = $this->foreColor; + $this->foreColor = $this->backgroundColor; + $this->backgroundColor = $tmp; + return $this; + } + + /** + * Set orientation of barcode and text + * @param float $value + * @return \Zend\Barcode\Object\ObjectInterface + * @throws \Zend\Barcode\Object\Exception\ExceptionInterface + */ + public function setOrientation($value) + { + $this->orientation = floatval($value) - floor(floatval($value) / 360) * 360; + return $this; + } + + /** + * Retrieve orientation of barcode and text + * @return float + */ + public function getOrientation() + { + return $this->orientation; + } + + /** + * Set text to encode + * @param string $value + * @return \Zend\Barcode\Object\ObjectInterface + */ + public function setText($value) + { + $this->text = trim($value); + return $this; + } + + /** + * Retrieve text to encode + * @return string + */ + public function getText() + { + $text = $this->text; + if ($this->withChecksum) { + $text .= $this->getChecksum($this->text); + } + return $this->addLeadingZeros($text); + } + + /** + * Automatically add leading zeros if barcode length is fixed + * @param string $text + * @param bool $withoutChecksum + * @return string + */ + protected function addLeadingZeros($text, $withoutChecksum = false) + { + if ($this->barcodeLength && $this->addLeadingZeros) { + $omitChecksum = (int) ($this->withChecksum && $withoutChecksum); + if (is_int($this->barcodeLength)) { + $length = $this->barcodeLength - $omitChecksum; + if (strlen($text) < $length) { + $text = str_repeat('0', $length - strlen($text)) . $text; + } + } else { + if ($this->barcodeLength == 'even') { + $text = ((strlen($text) - $omitChecksum) % 2 ? '0' . $text : $text); + } + } + } + return $text; + } + + /** + * Retrieve text to encode + * @return string + */ + public function getRawText() + { + return $this->text; + } + + /** + * Retrieve text to display + * @return string + */ + public function getTextToDisplay() + { + if ($this->withChecksumInText) { + return $this->getText(); + } + + return $this->addLeadingZeros($this->text, true); + } + + /** + * Activate/deactivate drawing of text to encode + * @param bool $value + * @return \Zend\Barcode\Object\ObjectInterface + */ + public function setDrawText($value) + { + $this->drawText = (bool) $value; + return $this; + } + + /** + * Retrieve if drawing of text to encode is enabled + * @return bool + */ + public function getDrawText() + { + return $this->drawText; + } + + /** + * Activate/deactivate the adjustment of the position + * of the characters to the position of the bars + * @param bool $value + * @return \Zend\Barcode\Object\ObjectInterface + * @throws \Zend\Barcode\Object\Exception\ExceptionInterface + */ + public function setStretchText($value) + { + $this->stretchText = (bool) $value; + return $this; + } + + /** + * Retrieve if the adjustment of the position of the characters + * to the position of the bars is enabled + * @return bool + */ + public function getStretchText() + { + return $this->stretchText; + } + + /** + * Activate/deactivate the automatic generation + * of the checksum character + * added to the barcode text + * @param bool $value + * @return \Zend\Barcode\Object\ObjectInterface + */ + public function setWithChecksum($value) + { + if (!$this->mandatoryChecksum) { + $this->withChecksum = (bool) $value; + } + return $this; + } + + /** + * Retrieve if the checksum character is automatically + * added to the barcode text + * @return bool + */ + public function getWithChecksum() + { + return $this->withChecksum; + } + + /** + * Activate/deactivate the automatic generation + * of the checksum character + * added to the barcode text + * @param bool $value + * @return \Zend\Barcode\Object\ObjectInterface + * @throws \Zend\Barcode\Object\Exception\ExceptionInterface + */ + public function setWithChecksumInText($value) + { + if (!$this->mandatoryChecksum) { + $this->withChecksumInText = (bool) $value; + } + return $this; + } + + /** + * Retrieve if the checksum character is automatically + * added to the barcode text + * @return bool + */ + public function getWithChecksumInText() + { + return $this->withChecksumInText; + } + + /** + * Set the font: + * - if integer between 1 and 5, use gd built-in fonts + * - if string, $value is assumed to be the path to a TTF font + * @param int|string $value + * @return \Zend\Barcode\Object\ObjectInterface + * @throws \Zend\Barcode\Object\Exception\ExceptionInterface + */ + public function setFont($value) + { + if (is_int($value) && $value >= 1 && $value <= 5) { + if (!extension_loaded('gd')) { + throw new Exception\ExtensionNotLoadedException( + 'GD extension is required to use numeric font' + ); + } + + // Case of numeric font with GD + $this->font = $value; + + // In this case font size is given by: + $this->fontSize = imagefontheight($value); + } elseif (is_string($value)) { + $this->font = $value; + } else { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid font "%s" provided to setFont()', + $value + )); + } + return $this; + } + + /** + * Retrieve the font + * @return int|string + */ + public function getFont() + { + return $this->font; + } + + /** + * Set the size of the font in case of TTF + * @param float $value + * @return \Zend\Barcode\Object\ObjectInterface + * @throws \Zend\Barcode\Object\Exception\ExceptionInterface + */ + public function setFontSize($value) + { + if (is_numeric($this->font)) { + // Case of numeric font with GD + return $this; + } + + if (!is_numeric($value)) { + throw new Exception\InvalidArgumentException( + 'Font size must be a numeric value' + ); + } + + $this->fontSize = $value; + return $this; + } + + /** + * Retrieve the size of the font in case of TTF + * @return float + */ + public function getFontSize() + { + return $this->fontSize; + } + + /** + * Quiet zone before first bar + * and after the last bar + * @return int + */ + public function getQuietZone() + { + if ($this->withQuietZones || $this->mandatoryQuietZones) { + return 10 * $this->barThinWidth * $this->factor; + } + + return 0; + } + + /** + * Add an instruction in the array of instructions + * @param array $instruction + */ + protected function addInstruction(array $instruction) + { + $this->instructions[] = $instruction; + } + + /** + * Retrieve the set of drawing instructions + * @return array + */ + public function getInstructions() + { + return $this->instructions; + } + + /** + * Add a polygon drawing instruction in the set of instructions + * @param array $points + * @param int $color + * @param bool $filled + */ + protected function addPolygon(array $points, $color = null, $filled = true) + { + if ($color === null) { + $color = $this->foreColor; + } + $this->addInstruction(array( + 'type' => 'polygon', + 'points' => $points, + 'color' => $color, + 'filled' => $filled, + )); + } + + /** + * Add a text drawing instruction in the set of instructions + * @param string $text + * @param float $size + * @param int[] $position + * @param string $font + * @param int $color + * @param string $alignment + * @param float $orientation + */ + protected function addText( + $text, + $size, + $position, + $font, + $color, + $alignment = 'center', + $orientation = 0 + ) { + if ($color === null) { + $color = $this->foreColor; + } + $this->addInstruction(array( + 'type' => 'text', + 'text' => $text, + 'size' => $size, + 'position' => $position, + 'font' => $font, + 'color' => $color, + 'alignment' => $alignment, + 'orientation' => $orientation, + )); + } + + /** + * Checking of parameters after all settings + * @return bool + */ + public function checkParams() + { + $this->checkText(); + $this->checkFontAndOrientation(); + $this->checkSpecificParams(); + return true; + } + + /** + * Check if a text is really provided to barcode + * @param string|null $value + * @return void + * @throws \Zend\Barcode\Object\Exception\ExceptionInterface + */ + protected function checkText($value = null) + { + if ($value === null) { + $value = $this->text; + } + if (!strlen($value)) { + throw new Exception\RuntimeException( + 'A text must be provide to Barcode before drawing' + ); + } + $this->validateText($value); + } + + /** + * Check the ratio between the thick and the thin bar + * @param int $min + * @param int $max + * @return void + * @throws \Zend\Barcode\Object\Exception\ExceptionInterface + */ + protected function checkRatio($min = 2, $max = 3) + { + $ratio = $this->barThickWidth / $this->barThinWidth; + if (!($ratio >= $min && $ratio <= $max)) { + throw new Exception\OutOfRangeException(sprintf( + 'Ratio thick/thin bar must be between %0.1f and %0.1f (actual %0.3f)', + $min, + $max, + $ratio + )); + } + } + + /** + * Drawing with an angle is just allow TTF font + * @return void + * @throws \Zend\Barcode\Object\Exception\ExceptionInterface + */ + protected function checkFontAndOrientation() + { + if (is_numeric($this->font) && $this->orientation != 0) { + throw new Exception\RuntimeException( + 'Only drawing with TTF font allow orientation of the barcode.' + ); + } + } + + /** + * Width of the result image + * (before any rotation) + * @return int + */ + protected function calculateWidth() + { + return (int) $this->withBorder + + $this->calculateBarcodeWidth() + + (int) $this->withBorder; + } + + /** + * Calculate the width of the barcode + * @return int + */ + abstract protected function calculateBarcodeWidth(); + + /** + * Height of the result object + * @return int + */ + protected function calculateHeight() + { + return (int) $this->withBorder * 2 + + $this->calculateBarcodeHeight() + + (int) $this->withBorder * 2; + } + + /** + * Height of the barcode + * @return int + */ + protected function calculateBarcodeHeight() + { + $textHeight = 0; + $extraHeight = 0; + if ($this->drawText) { + $textHeight += $this->fontSize; + $extraHeight = 2; + } + return ($this->barHeight + $textHeight) * $this->factor + $extraHeight; + } + + /** + * Get height of the result object + * @param bool $recalculate + * @return int + */ + public function getHeight($recalculate = false) + { + if ($this->height === null || $recalculate) { + $this->height = + abs($this->calculateHeight() * cos($this->orientation / 180 * pi())) + + abs($this->calculateWidth() * sin($this->orientation / 180 * pi())); + } + return $this->height; + } + + /** + * Get width of the result object + * @param bool $recalculate + * @return int + */ + public function getWidth($recalculate = false) + { + if ($this->width === null || $recalculate) { + $this->width = + abs($this->calculateWidth() * cos($this->orientation / 180 * pi())) + + abs($this->calculateHeight() * sin($this->orientation / 180 * pi())); + } + return $this->width; + } + + /** + * Calculate the offset from the left of the object + * if an orientation is activated + * @param bool $recalculate + * @return float + */ + public function getOffsetLeft($recalculate = false) + { + if ($this->offsetLeft === null || $recalculate) { + $this->offsetLeft = - min( + array( + 0 * cos($this->orientation / 180 * pi()) - 0 * sin($this->orientation / 180 * pi()), + 0 * cos($this->orientation / 180 * pi()) - $this->calculateBarcodeHeight() * sin($this->orientation / 180 * pi()), + $this->calculateBarcodeWidth() * cos($this->orientation / 180 * pi()) - $this->calculateBarcodeHeight() * sin($this->orientation / 180 * pi()), + $this->calculateBarcodeWidth() * cos($this->orientation / 180 * pi()) - 0 * sin($this->orientation / 180 * pi()), + ) + ); + } + return $this->offsetLeft; + } + + /** + * Calculate the offset from the top of the object + * if an orientation is activated + * @param bool $recalculate + * @return float + */ + public function getOffsetTop($recalculate = false) + { + if ($this->offsetTop === null || $recalculate) { + $this->offsetTop = - min( + array( + 0 * cos($this->orientation / 180 * pi()) + 0 * sin($this->orientation / 180 * pi()), + $this->calculateBarcodeHeight() * cos($this->orientation / 180 * pi()) + 0 * sin($this->orientation / 180 * pi()), + $this->calculateBarcodeHeight() * cos($this->orientation / 180 * pi()) + $this->calculateBarcodeWidth() * sin($this->orientation / 180 * pi()), + 0 * cos($this->orientation / 180 * pi()) + $this->calculateBarcodeWidth() * sin($this->orientation / 180 * pi()), + ) + ); + } + return $this->offsetTop; + } + + /** + * Apply rotation on a point in X/Y dimensions + * @param float $x1 x-position before rotation + * @param float $y1 y-position before rotation + * @return int[] Array of two elements corresponding to the new XY point + */ + protected function rotate($x1, $y1) + { + $x2 = $x1 * cos($this->orientation / 180 * pi()) + - $y1 * sin($this->orientation / 180 * pi()) + + $this->getOffsetLeft(); + $y2 = $y1 * cos($this->orientation / 180 * pi()) + + $x1 * sin($this->orientation / 180 * pi()) + + $this->getOffsetTop(); + return array(intval($x2), intval($y2)); + } + + /** + * Complete drawing of the barcode + * @return array Table of instructions + */ + public function draw() + { + $this->checkParams(); + $this->drawBarcode(); + $this->drawBorder(); + $this->drawText(); + return $this->getInstructions(); + } + + /** + * Draw the barcode + * @return void + */ + protected function drawBarcode() + { + $barcodeTable = $this->prepareBarcode(); + + $this->preDrawBarcode(); + + $xpos = (int) $this->withBorder; + $ypos = (int) $this->withBorder; + + $point1 = $this->rotate(0, 0); + $point2 = $this->rotate(0, $this->calculateHeight() - 1); + $point3 = $this->rotate( + $this->calculateWidth() - 1, + $this->calculateHeight() - 1 + ); + $point4 = $this->rotate($this->calculateWidth() - 1, 0); + + $this->addPolygon(array( + $point1, + $point2, + $point3, + $point4 + ), $this->backgroundColor); + + $xpos += $this->getQuietZone(); + $barLength = $this->barHeight * $this->factor; + + foreach ($barcodeTable as $bar) { + $width = $bar[1] * $this->factor; + if ($bar[0]) { + $point1 = $this->rotate($xpos, $ypos + $bar[2] * $barLength); + $point2 = $this->rotate($xpos, $ypos + $bar[3] * $barLength); + $point3 = $this->rotate( + $xpos + $width - 1, + $ypos + $bar[3] * $barLength + ); + $point4 = $this->rotate( + $xpos + $width - 1, + $ypos + $bar[2] * $barLength + ); + $this->addPolygon(array( + $point1, + $point2, + $point3, + $point4, + )); + } + $xpos += $width; + } + + $this->postDrawBarcode(); + } + + /** + * Partial function to draw border + * @return void + */ + protected function drawBorder() + { + if ($this->withBorder) { + $point1 = $this->rotate(0, 0); + $point2 = $this->rotate($this->calculateWidth() - 1, 0); + $point3 = $this->rotate( + $this->calculateWidth() - 1, + $this->calculateHeight() - 1 + ); + $point4 = $this->rotate(0, $this->calculateHeight() - 1); + $this->addPolygon(array( + $point1, + $point2, + $point3, + $point4, + $point1, + ), $this->foreColor, false); + } + } + + /** + * Partial function to draw text + * @return void + */ + protected function drawText() + { + if ($this->drawText) { + $text = $this->getTextToDisplay(); + if ($this->stretchText) { + $textLength = strlen($text); + $space = ($this->calculateWidth() - 2 * $this->getQuietZone()) / $textLength; + for ($i = 0; $i < $textLength; $i ++) { + $leftPosition = $this->getQuietZone() + $space * ($i + 0.5); + $this->addText( + $text{$i}, + $this->fontSize * $this->factor, + $this->rotate( + $leftPosition, + (int) $this->withBorder * 2 + $this->factor * ($this->barHeight + $this->fontSize) + 1 + ), + $this->font, + $this->foreColor, + 'center', + - $this->orientation + ); + } + } else { + $this->addText( + $text, + $this->fontSize * $this->factor, + $this->rotate( + $this->calculateWidth() / 2, + (int) $this->withBorder * 2 + $this->factor * ($this->barHeight + $this->fontSize) + 1 + ), + $this->font, + $this->foreColor, + 'center', + - $this->orientation + ); + } + } + } + + /** + * Check for invalid characters + * @param string $value Text to be checked + * @return void + */ + public function validateText($value) + { + $this->validateSpecificText($value); + } + + /** + * Standard validation for most of barcode objects + * @param string $value + * @param array $options + */ + protected function validateSpecificText($value, $options = array()) + { + $validatorName = (isset($options['validator'])) ? $options['validator'] : $this->getType(); + + $validator = new BarcodeValidator(array( + 'adapter' => $validatorName, + 'usechecksum' => false, + )); + + $checksumCharacter = ''; + $withChecksum = false; + if ($this->mandatoryChecksum) { + $checksumCharacter = $this->substituteChecksumCharacter; + $withChecksum = true; + } + + $value = $this->addLeadingZeros($value, $withChecksum) . $checksumCharacter; + + if (!$validator->isValid($value)) { + $message = implode("\n", $validator->getMessages()); + throw new Exception\BarcodeValidationException($message); + } + } + + /** + * Each child must prepare the barcode and return + * a table like array( + * 0 => array( + * 0 => int (visible(black) or not(white)) + * 1 => int (width of the bar) + * 2 => float (0->1 position from the top of the beginning of the bar in %) + * 3 => float (0->1 position from the top of the end of the bar in %) + * ), + * 1 => ... + * ) + * + * @return array + */ + abstract protected function prepareBarcode(); + + /** + * Checking of parameters after all settings + * + * @return void + */ + abstract protected function checkSpecificParams(); + + /** + * Allow each child to draw something else + * + * @return void + */ + protected function preDrawBarcode() + { + } + + /** + * Allow each child to draw something else + * (ex: bearer bars in interleaved 2 of 5 code) + * + * @return void + */ + protected function postDrawBarcode() + { + } +} diff --git a/library/Zend/Barcode/Object/Codabar.php b/library/Zend/Barcode/Object/Codabar.php new file mode 100755 index 0000000000..71120c6015 --- /dev/null +++ b/library/Zend/Barcode/Object/Codabar.php @@ -0,0 +1,77 @@ + "101010011", '1' => "101011001", '2' => "101001011", + '3' => "110010101", '4' => "101101001", '5' => "110101001", + '6' => "100101011", '7' => "100101101", '8' => "100110101", + '9' => "110100101", '-' => "101001101", '$' => "101100101", + ':' => "1101011011", '/' => "1101101011", '.' => "1101101101", + '+' => "1011011011", 'A' => "1011001001", 'B' => "1010010011", + 'C' => "1001001011", 'D' => "1010011001" + ); + + /** + * Width of the barcode (in pixels) + * @return int + */ + protected function calculateBarcodeWidth() + { + $quietZone = $this->getQuietZone(); + $encodedData = 0; + $barcodeChar = str_split($this->getText()); + if (count($barcodeChar) > 1) { + foreach ($barcodeChar as $c) { + $encodedData += ((strlen($this->codingMap[$c]) + 1) * $this->barThinWidth) * $this->factor; + } + } + $encodedData -= (1 * $this->barThinWidth * $this->factor); + return $quietZone + $encodedData + $quietZone; + } + + /** + * Partial check of Codabar barcode + * @return void + */ + protected function checkSpecificParams() + { + } + + /** + * Prepare array to draw barcode + * @return array + */ + protected function prepareBarcode() + { + $text = str_split($this->getText()); + $barcodeTable = array(); + foreach ($text as $char) { + $barcodeChar = str_split($this->codingMap[$char]); + foreach ($barcodeChar as $c) { + // visible, width, top, length + $barcodeTable[] = array($c, $this->barThinWidth, 0, 1); + } + $barcodeTable[] = array(0, $this->barThinWidth); + } + return $barcodeTable; + } +} diff --git a/library/Zend/Barcode/Object/Code128.php b/library/Zend/Barcode/Object/Code128.php new file mode 100755 index 0000000000..3bb0cc0c75 --- /dev/null +++ b/library/Zend/Barcode/Object/Code128.php @@ -0,0 +1,311 @@ + "11011001100", 1 => "11001101100", 2 => "11001100110", + 3 => "10010011000", 4 => "10010001100", 5 => "10001001100", + 6 => "10011001000", 7 => "10011000100", 8 => "10001100100", + 9 => "11001001000", 10 => "11001000100", 11 => "11000100100", + 12 => "10110011100", 13 => "10011011100", 14 => "10011001110", + 15 => "10111001100", 16 => "10011101100", 17 => "10011100110", + 18 => "11001110010", 19 => "11001011100", 20 => "11001001110", + 21 => "11011100100", 22 => "11001110100", 23 => "11101101110", + 24 => "11101001100", 25 => "11100101100", 26 => "11100100110", + 27 => "11101100100", 28 => "11100110100", 29 => "11100110010", + 30 => "11011011000", 31 => "11011000110", 32 => "11000110110", + 33 => "10100011000", 34 => "10001011000", 35 => "10001000110", + 36 => "10110001000", 37 => "10001101000", 38 => "10001100010", + 39 => "11010001000", 40 => "11000101000", 41 => "11000100010", + 42 => "10110111000", 43 => "10110001110", 44 => "10001101110", + 45 => "10111011000", 46 => "10111000110", 47 => "10001110110", + 48 => "11101110110", 49 => "11010001110", 50 => "11000101110", + 51 => "11011101000", 52 => "11011100010", 53 => "11011101110", + 54 => "11101011000", 55 => "11101000110", 56 => "11100010110", + 57 => "11101101000", 58 => "11101100010", 59 => "11100011010", + 60 => "11101111010", 61 => "11001000010", 62 => "11110001010", + 63 => "10100110000", 64 => "10100001100", 65 => "10010110000", + 66 => "10010000110", 67 => "10000101100", 68 => "10000100110", + 69 => "10110010000", 70 => "10110000100", 71 => "10011010000", + 72 => "10011000010", 73 => "10000110100", 74 => "10000110010", + 75 => "11000010010", 76 => "11001010000", 77 => "11110111010", + 78 => "11000010100", 79 => "10001111010", 80 => "10100111100", + 81 => "10010111100", 82 => "10010011110", 83 => "10111100100", + 84 => "10011110100", 85 => "10011110010", 86 => "11110100100", + 87 => "11110010100", 88 => "11110010010", 89 => "11011011110", + 90 => "11011110110", 91 => "11110110110", 92 => "10101111000", + 93 => "10100011110", 94 => "10001011110", 95 => "10111101000", + 96 => "10111100010", 97 => "11110101000", 98 => "11110100010", + 99 => "10111011110", 100 => "10111101110", 101 => "11101011110", + 102 => "11110101110", + 103 => "11010000100", 104 => "11010010000", 105 => "11010011100", + 106 => "1100011101011"); + + /** + * Character sets ABC + * @var array + */ + protected $charSets = array( + 'A' => array( + ' ', '!', '"', '#', '$', '%', '&', "'", + '(', ')', '*', '+', ',', '-', '.', '/', + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', ':', ';', '<', '=', '>', '?', + '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', + 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', + 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + 'FNC3', 'FNC2', 'SHIFT', 'Code C', 'Code B', 'FNC4', 'FNC1', + 'START A', 'START B', 'START C', 'STOP'), + 'B' => array( + ' ', '!', '"', '#', '$', '%', '&', "'", + '(', ')', '*', '+', ',', '-', '.', '/', + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', ':', ';', '<', '=', '>', '?', + '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', + 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', + 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', + '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', + 'x', 'y', 'z', '{', '|', '}', '~', 0x7F, + 'FNC3', 'FNC2', 'SHIFT', 'Code C', 'FNC4', 'Code A', 'FNC1', + 'START A', 'START B', 'START C', 'STOP',), + 'C' => array( + '00', '01', '02', '03', '04', '05', '06', '07', '08', '09', + '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', + '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', + '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', + '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', + '50', '51', '52', '53', '54', '55', '56', '57', '58', '59', + '60', '61', '62', '63', '64', '65', '66', '67', '68', '69', + '70', '71', '72', '73', '74', '75', '76', '77', '78', '79', + '80', '81', '82', '83', '84', '85', '86', '87', '88', '89', + '90', '91', '92', '93', '94', '95', '96', '97', '98', '99', + 'Code B', 'Code A', 'FNC1', 'START A', 'START B', 'START C', 'STOP')); + + /** + * Width of the barcode (in pixels) + * @return int + */ + protected function calculateBarcodeWidth() + { + $quietZone = $this->getQuietZone(); + // Each characters contain 11 bars... + $characterLength = 11 * $this->barThinWidth * $this->factor; + $convertedChars = count($this->convertToBarcodeChars($this->getText())); + if ($this->withChecksum) { + $convertedChars++; + } + $encodedData = $convertedChars * $characterLength; + // ...except the STOP character (13) + $encodedData += $characterLength + 2 * $this->barThinWidth * $this->factor; + $width = $quietZone + $encodedData + $quietZone; + return $width; + } + + /** + * Partial check of code128 barcode + * @return void + */ + protected function checkSpecificParams() + { + } + + /** + * Prepare array to draw barcode + * @return array + */ + protected function prepareBarcode() + { + $barcodeTable = array(); + + $convertedChars = $this->convertToBarcodeChars($this->getText()); + + if ($this->withChecksum) { + $convertedChars[] = $this->getChecksum($this->getText()); + } + + // STOP CHARACTER + $convertedChars[] = 106; + + foreach ($convertedChars as $barcodeChar) { + $barcodePattern = $this->codingMap[$barcodeChar]; + foreach (str_split($barcodePattern) as $c) { + $barcodeTable[] = array($c, $this->barThinWidth, 0, 1); + } + } + return $barcodeTable; + } + + /** + * Checks if the next $length chars of $string starting at $pos are numeric. + * Returns false if the end of the string is reached. + * @param string $string String to search + * @param int $pos Starting position + * @param int $length Length to search + * @return bool + */ + protected static function _isDigit($string, $pos, $length = 2) + { + if ($pos + $length > strlen($string)) { + return false; + } + + for ($i = $pos; $i < $pos + $length; $i++) { + if (!is_numeric($string[$i])) { + return false; + } + } + return true; + } + + /** + * Convert string to barcode string + * @param string $string + * @return array + */ + protected function convertToBarcodeChars($string) + { + $string = (string) $string; + if (!strlen($string)) { + return array(); + } + + if (isset($this->convertedText[md5($string)])) { + return $this->convertedText[md5($string)]; + } + + $currentCharset = null; + $result = array(); + + $strlen = strlen($string); + for ($pos = 0; $pos < $strlen; $pos++) { + $char = $string[$pos]; + + if (static::_isDigit($string, $pos, 4) && $currentCharset != 'C' + || static::_isDigit($string, $pos, 2) && $currentCharset == 'C') { + /** + * Switch to C if the next 4 chars are numeric or stay C if the next 2 + * chars are numeric + */ + if ($currentCharset != 'C') { + if ($pos == 0) { + $code = array_search("START C", $this->charSets['C']); + } else { + $code = array_search("Code C", $this->charSets[$currentCharset]); + } + $result[] = $code; + $currentCharset = 'C'; + } + } elseif (in_array($char, $this->charSets['B']) && $currentCharset != 'B' + && !(in_array($char, $this->charSets['A']) && $currentCharset == 'A')) { + /** + * Switch to B as B contains the char and B is not the current charset. + */ + if ($pos == 0) { + $code = array_search("START B", $this->charSets['B']); + } else { + $code = array_search("Code B", $this->charSets[$currentCharset]); + } + $result[] = $code; + $currentCharset = 'B'; + } elseif (array_key_exists($char, $this->charSets['A']) && $currentCharset != 'A' + && !(array_key_exists($char, $this->charSets['B']) && $currentCharset == 'B')) { + /** + * Switch to C as C contains the char and C is not the current charset. + */ + if ($pos == 0) { + $code = array_search("START A", $this->charSets['A']); + } else { + $code = array_search("Code A", $this->charSets[$currentCharset]); + } + $result[] = $code; + $currentCharset = 'A'; + } + + if ($currentCharset == 'C') { + $code = array_search(substr($string, $pos, 2), $this->charSets['C']); + $pos++; //Two chars from input + } else { + $code = array_search($string[$pos], $this->charSets[$currentCharset]); + } + $result[] = $code; + } + + $this->convertedText[md5($string)] = $result; + return $result; + } + + /** + * Set text to encode + * @param string $value + * @return Code128 + */ + public function setText($value) + { + $this->text = $value; + return $this; + } + + /** + * Retrieve text to encode + * @return string + */ + public function getText() + { + return $this->text; + } + + /** + * Get barcode checksum + * + * @param string $text + * @return int + */ + public function getChecksum($text) + { + $tableOfChars = $this->convertToBarcodeChars($text); + + $sum = $tableOfChars[0]; + unset($tableOfChars[0]); + + $k = 1; + foreach ($tableOfChars as $char) { + $sum += ($k++) * $char; + } + + $checksum = $sum % 103; + + return $checksum; + } +} diff --git a/library/Zend/Barcode/Object/Code25.php b/library/Zend/Barcode/Object/Code25.php new file mode 100755 index 0000000000..2f32ca7af0 --- /dev/null +++ b/library/Zend/Barcode/Object/Code25.php @@ -0,0 +1,117 @@ + '00110', + '1' => '10001', + '2' => '01001', + '3' => '11000', + '4' => '00101', + '5' => '10100', + '6' => '01100', + '7' => '00011', + '8' => '10010', + '9' => '01010', + ); + + /** + * Width of the barcode (in pixels) + * @return int + */ + protected function calculateBarcodeWidth() + { + $quietZone = $this->getQuietZone(); + $startCharacter = (2 * $this->barThickWidth + 4 * $this->barThinWidth) * $this->factor; + $characterLength = (3 * $this->barThinWidth + 2 * $this->barThickWidth + 5 * $this->barThinWidth) + * $this->factor; + $encodedData = strlen($this->getText()) * $characterLength; + $stopCharacter = (2 * $this->barThickWidth + 4 * $this->barThinWidth) * $this->factor; + return $quietZone + $startCharacter + $encodedData + $stopCharacter + $quietZone; + } + + /** + * Partial check of interleaved 2 of 5 barcode + * @return void + */ + protected function checkSpecificParams() + { + $this->checkRatio(); + } + + /** + * Prepare array to draw barcode + * @return array + */ + protected function prepareBarcode() + { + $barcodeTable = array(); + + // Start character (30301) + $barcodeTable[] = array(1, $this->barThickWidth, 0, 1); + $barcodeTable[] = array(0, $this->barThinWidth, 0, 1); + $barcodeTable[] = array(1, $this->barThickWidth, 0, 1); + $barcodeTable[] = array(0, $this->barThinWidth, 0, 1); + $barcodeTable[] = array(1, $this->barThinWidth, 0, 1); + $barcodeTable[] = array(0, $this->barThinWidth); + + $text = str_split($this->getText()); + foreach ($text as $char) { + $barcodeChar = str_split($this->codingMap[$char]); + foreach ($barcodeChar as $c) { + /* visible, width, top, length */ + $width = $c ? $this->barThickWidth : $this->barThinWidth; + $barcodeTable[] = array(1, $width, 0, 1); + $barcodeTable[] = array(0, $this->barThinWidth); + } + } + + // Stop character (30103) + $barcodeTable[] = array(1, $this->barThickWidth, 0, 1); + $barcodeTable[] = array(0, $this->barThinWidth, 0, 1); + $barcodeTable[] = array(1, $this->barThinWidth, 0, 1); + $barcodeTable[] = array(0, $this->barThinWidth, 0, 1); + $barcodeTable[] = array(1, $this->barThickWidth, 0, 1); + return $barcodeTable; + } + + /** + * Get barcode checksum + * + * @param string $text + * @return int + */ + public function getChecksum($text) + { + $this->checkText($text); + $factor = 3; + $checksum = 0; + + for ($i = strlen($text); $i > 0; $i --) { + $checksum += intval($text{$i - 1}) * $factor; + $factor = 4 - $factor; + } + + $checksum = (10 - ($checksum % 10)) % 10; + + return $checksum; + } +} diff --git a/library/Zend/Barcode/Object/Code25interleaved.php b/library/Zend/Barcode/Object/Code25interleaved.php new file mode 100755 index 0000000000..0fb3ec33a1 --- /dev/null +++ b/library/Zend/Barcode/Object/Code25interleaved.php @@ -0,0 +1,159 @@ +barcodeLength = 'even'; + } + + /** + * Activate/deactivate drawing of bearer bars + * @param bool $value + * @return Code25 + */ + public function setWithBearerBars($value) + { + $this->withBearerBars = (bool) $value; + return $this; + } + + /** + * Retrieve if bearer bars are enabled + * @return bool + */ + public function getWithBearerBars() + { + return $this->withBearerBars; + } + + /** + * Width of the barcode (in pixels) + * @return int + */ + protected function calculateBarcodeWidth() + { + $quietZone = $this->getQuietZone(); + $startCharacter = (4 * $this->barThinWidth) * $this->factor; + $characterLength = (3 * $this->barThinWidth + 2 * $this->barThickWidth) * $this->factor; + $encodedData = strlen($this->getText()) * $characterLength; + $stopCharacter = ($this->barThickWidth + 2 * $this->barThinWidth) * $this->factor; + return $quietZone + $startCharacter + $encodedData + $stopCharacter + $quietZone; + } + + /** + * Prepare array to draw barcode + * @return array + */ + protected function prepareBarcode() + { + if ($this->withBearerBars) { + $this->withBorder = false; + } + + $barcodeTable = array(); + + // Start character (0000) + $barcodeTable[] = array(1, $this->barThinWidth, 0, 1); + $barcodeTable[] = array(0, $this->barThinWidth, 0, 1); + $barcodeTable[] = array(1, $this->barThinWidth, 0, 1); + $barcodeTable[] = array(0, $this->barThinWidth, 0, 1); + + // Encoded $text + $text = $this->getText(); + for ($i = 0, $len = strlen($text); $i < $len; $i += 2) { // Draw 2 chars at a time + $char1 = substr($text, $i, 1); + $char2 = substr($text, $i + 1, 1); + + // Interleave + for ($ibar = 0; $ibar < 5; $ibar ++) { + // Draws char1 bar (fore color) + $barWidth = (substr($this->codingMap[$char1], $ibar, 1)) + ? $this->barThickWidth + : $this->barThinWidth; + + $barcodeTable[] = array(1, $barWidth, 0, 1); + + // Left space corresponding to char2 (background color) + $barWidth = (substr($this->codingMap[$char2], $ibar, 1)) + ? $this->barThickWidth + : $this->barThinWidth; + $barcodeTable[] = array(0, $barWidth, 0, 1); + } + } + + // Stop character (100) + $barcodeTable[] = array(1, $this->barThickWidth, 0, 1); + $barcodeTable[] = array(0, $this->barThinWidth, 0, 1); + $barcodeTable[] = array(1, $this->barThinWidth, 0, 1); + return $barcodeTable; + } + + /** + * Drawing of bearer bars (if enabled) + * + * @return void + */ + protected function postDrawBarcode() + { + if (!$this->withBearerBars) { + return; + } + + $width = $this->barThickWidth * $this->factor; + $point1 = $this->rotate(-1, -1); + $point2 = $this->rotate($this->calculateWidth() - 1, -1); + $point3 = $this->rotate($this->calculateWidth() - 1, $width - 1); + $point4 = $this->rotate(-1, $width - 1); + $this->addPolygon(array( + $point1, + $point2, + $point3, + $point4, + )); + $point1 = $this->rotate( + 0, + 0 + $this->barHeight * $this->factor - 1 + ); + $point2 = $this->rotate( + $this->calculateWidth() - 1, + 0 + $this->barHeight * $this->factor - 1 + ); + $point3 = $this->rotate( + $this->calculateWidth() - 1, + 0 + $this->barHeight * $this->factor - $width + ); + $point4 = $this->rotate( + 0, + 0 + $this->barHeight * $this->factor - $width + ); + $this->addPolygon(array( + $point1, + $point2, + $point3, + $point4, + )); + } +} diff --git a/library/Zend/Barcode/Object/Code39.php b/library/Zend/Barcode/Object/Code39.php new file mode 100755 index 0000000000..c70e289eb5 --- /dev/null +++ b/library/Zend/Barcode/Object/Code39.php @@ -0,0 +1,162 @@ + '000110100', + '1' => '100100001', + '2' => '001100001', + '3' => '101100000', + '4' => '000110001', + '5' => '100110000', + '6' => '001110000', + '7' => '000100101', + '8' => '100100100', + '9' => '001100100', + 'A' => '100001001', + 'B' => '001001001', + 'C' => '101001000', + 'D' => '000011001', + 'E' => '100011000', + 'F' => '001011000', + 'G' => '000001101', + 'H' => '100001100', + 'I' => '001001100', + 'J' => '000011100', + 'K' => '100000011', + 'L' => '001000011', + 'M' => '101000010', + 'N' => '000010011', + 'O' => '100010010', + 'P' => '001010010', + 'Q' => '000000111', + 'R' => '100000110', + 'S' => '001000110', + 'T' => '000010110', + 'U' => '110000001', + 'V' => '011000001', + 'W' => '111000000', + 'X' => '010010001', + 'Y' => '110010000', + 'Z' => '011010000', + '-' => '010000101', + '.' => '110000100', + ' ' => '011000100', + '$' => '010101000', + '/' => '010100010', + '+' => '010001010', + '%' => '000101010', + '*' => '010010100', + ); + + /** + * Partial check of Code39 barcode + * @return void + */ + protected function checkSpecificParams() + { + $this->checkRatio(); + } + + /** + * Width of the barcode (in pixels) + * @return int + */ + protected function calculateBarcodeWidth() + { + $quietZone = $this->getQuietZone(); + $characterLength = (6 * $this->barThinWidth + 3 * $this->barThickWidth + 1) * $this->factor; + $encodedData = strlen($this->getText()) * $characterLength - $this->factor; + return $quietZone + $encodedData + $quietZone; + } + + /** + * Set text to encode + * @param string $value + * @return Code39 + */ + public function setText($value) + { + $this->text = $value; + return $this; + } + + /** + * Retrieve text to display + * @return string + */ + public function getText() + { + return '*' . parent::getText() . '*'; + } + + /** + * Retrieve text to display + * @return string + */ + public function getTextToDisplay() + { + $text = parent::getTextToDisplay(); + if (substr($text, 0, 1) != '*' && substr($text, -1) != '*') { + return '*' . $text . '*'; + } + + return $text; + } + + /** + * Prepare array to draw barcode + * @return array + */ + protected function prepareBarcode() + { + $text = str_split($this->getText()); + $barcodeTable = array(); + foreach ($text as $char) { + $barcodeChar = str_split($this->codingMap[$char]); + $visible = true; + foreach ($barcodeChar as $c) { + /* visible, width, top, length */ + $width = $c ? $this->barThickWidth : $this->barThinWidth; + $barcodeTable[] = array((int) $visible, $width, 0, 1); + $visible = ! $visible; + } + $barcodeTable[] = array(0, $this->barThinWidth); + } + return $barcodeTable; + } + + /** + * Get barcode checksum + * + * @param string $text + * @return int + */ + public function getChecksum($text) + { + $this->checkText($text); + $text = str_split($text); + $charset = array_flip(array_keys($this->codingMap)); + $checksum = 0; + foreach ($text as $character) { + $checksum += $charset[$character]; + } + return array_search(($checksum % 43), $charset); + } +} diff --git a/library/Zend/Barcode/Object/Ean13.php b/library/Zend/Barcode/Object/Ean13.php new file mode 100755 index 0000000000..602668d97f --- /dev/null +++ b/library/Zend/Barcode/Object/Ean13.php @@ -0,0 +1,198 @@ + array( + 0 => "0001101", 1 => "0011001", 2 => "0010011", 3 => "0111101", 4 => "0100011", + 5 => "0110001", 6 => "0101111", 7 => "0111011", 8 => "0110111", 9 => "0001011" + ), + 'B' => array( + 0 => "0100111", 1 => "0110011", 2 => "0011011", 3 => "0100001", 4 => "0011101", + 5 => "0111001", 6 => "0000101", 7 => "0010001", 8 => "0001001", 9 => "0010111" + ), + 'C' => array( + 0 => "1110010", 1 => "1100110", 2 => "1101100", 3 => "1000010", 4 => "1011100", + 5 => "1001110", 6 => "1010000", 7 => "1000100", 8 => "1001000", 9 => "1110100" + )); + + protected $parities = array( + 0 => array('A','A','A','A','A','A'), + 1 => array('A','A','B','A','B','B'), + 2 => array('A','A','B','B','A','B'), + 3 => array('A','A','B','B','B','A'), + 4 => array('A','B','A','A','B','B'), + 5 => array('A','B','B','A','A','B'), + 6 => array('A','B','B','B','A','A'), + 7 => array('A','B','A','B','A','B'), + 8 => array('A','B','A','B','B','A'), + 9 => array('A','B','B','A','B','A') + ); + + /** + * Default options for Postnet barcode + * @return void + */ + protected function getDefaultOptions() + { + $this->barcodeLength = 13; + $this->mandatoryChecksum = true; + $this->mandatoryQuietZones = true; + } + + /** + * Width of the barcode (in pixels) + * @return int + */ + protected function calculateBarcodeWidth() + { + $quietZone = $this->getQuietZone(); + $startCharacter = (3 * $this->barThinWidth) * $this->factor; + $middleCharacter = (5 * $this->barThinWidth) * $this->factor; + $stopCharacter = (3 * $this->barThinWidth) * $this->factor; + $encodedData = (7 * $this->barThinWidth) * $this->factor * 12; + return $quietZone + $startCharacter + $middleCharacter + $encodedData + $stopCharacter + $quietZone; + } + + /** + * Partial check of interleaved EAN/UPC barcode + * @return void + */ + protected function checkSpecificParams() + { + } + + /** + * Prepare array to draw barcode + * @return array + */ + protected function prepareBarcode() + { + $barcodeTable = array(); + $height = ($this->drawText) ? 1.1 : 1; + + // Start character (101) + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(0, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + + $textTable = str_split($this->getText()); + $parity = $this->parities[$textTable[0]]; + + // First part + for ($i = 1; $i < 7; $i++) { + $bars = str_split($this->codingMap[$parity[$i - 1]][$textTable[$i]]); + foreach ($bars as $b) { + $barcodeTable[] = array($b, $this->barThinWidth, 0, 1); + } + } + + // Middle character (01010) + $barcodeTable[] = array(0, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(0, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(0, $this->barThinWidth, 0, $height); + + // Second part + for ($i = 7; $i < 13; $i++) { + $bars = str_split($this->codingMap['C'][$textTable[$i]]); + foreach ($bars as $b) { + $barcodeTable[] = array($b, $this->barThinWidth, 0, 1); + } + } + + // Stop character (101) + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(0, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + return $barcodeTable; + } + + /** + * Get barcode checksum + * + * @param string $text + * @return int + */ + public function getChecksum($text) + { + $this->checkText($text); + $factor = 3; + $checksum = 0; + + for ($i = strlen($text); $i > 0; $i --) { + $checksum += intval($text{$i - 1}) * $factor; + $factor = 4 - $factor; + } + + $checksum = (10 - ($checksum % 10)) % 10; + + return $checksum; + } + + /** + * Partial function to draw text + * @return void + */ + protected function drawText() + { + if (get_class($this) == 'Zend\Barcode\Object\Ean13') { + $this->drawEan13Text(); + } else { + parent::drawText(); + } + } + + protected function drawEan13Text() + { + if ($this->drawText) { + $text = $this->getTextToDisplay(); + $characterWidth = (7 * $this->barThinWidth) * $this->factor; + $leftPosition = $this->getQuietZone() - $characterWidth; + for ($i = 0; $i < $this->barcodeLength; $i ++) { + $this->addText( + $text{$i}, + $this->fontSize * $this->factor, + $this->rotate( + $leftPosition, + (int) $this->withBorder * 2 + $this->factor * ($this->barHeight + $this->fontSize) + 1 + ), + $this->font, + $this->foreColor, + 'left', + - $this->orientation + ); + switch ($i) { + case 0: + $factor = 3; + break; + case 6: + $factor = 4; + break; + default: + $factor = 0; + } + $leftPosition = $leftPosition + $characterWidth + ($factor * $this->barThinWidth * $this->factor); + } + } + } +} diff --git a/library/Zend/Barcode/Object/Ean2.php b/library/Zend/Barcode/Object/Ean2.php new file mode 100755 index 0000000000..b260a62d68 --- /dev/null +++ b/library/Zend/Barcode/Object/Ean2.php @@ -0,0 +1,38 @@ + array('A','A'), + 1 => array('A','B'), + 2 => array('B','A'), + 3 => array('B','B') + ); + + /** + * Default options for Ean2 barcode + * @return void + */ + protected function getDefaultOptions() + { + $this->barcodeLength = 2; + } + + protected function getParity($i) + { + $modulo = $this->getText() % 4; + return $this->parities[$modulo][$i]; + } +} diff --git a/library/Zend/Barcode/Object/Ean5.php b/library/Zend/Barcode/Object/Ean5.php new file mode 100755 index 0000000000..c04708bdc3 --- /dev/null +++ b/library/Zend/Barcode/Object/Ean5.php @@ -0,0 +1,124 @@ + array('B','B','A','A','A'), + 1 => array('B','A','B','A','A'), + 2 => array('B','A','A','B','A'), + 3 => array('B','A','A','A','B'), + 4 => array('A','B','B','A','A'), + 5 => array('A','A','B','B','A'), + 6 => array('A','A','A','B','B'), + 7 => array('A','B','A','B','A'), + 8 => array('A','B','A','A','B'), + 9 => array('A','A','B','A','B') + ); + + /** + * Default options for Ean5 barcode + * @return void + */ + protected function getDefaultOptions() + { + $this->barcodeLength = 5; + } + + /** + * Width of the barcode (in pixels) + * @return int + */ + protected function calculateBarcodeWidth() + { + $quietZone = $this->getQuietZone(); + $startCharacter = (5 * $this->barThinWidth) * $this->factor; + $middleCharacter = (2 * $this->barThinWidth) * $this->factor; + $encodedData = (7 * $this->barThinWidth) * $this->factor; + return $quietZone + $startCharacter + ($this->barcodeLength - 1) * $middleCharacter + $this->barcodeLength * $encodedData + $quietZone; + } + + /** + * Prepare array to draw barcode + * @return array + */ + protected function prepareBarcode() + { + $barcodeTable = array(); + + // Start character (01011) + $barcodeTable[] = array(0, $this->barThinWidth, 0, 1); + $barcodeTable[] = array(1, $this->barThinWidth, 0, 1); + $barcodeTable[] = array(0, $this->barThinWidth, 0, 1); + $barcodeTable[] = array(1, $this->barThinWidth, 0, 1); + $barcodeTable[] = array(1, $this->barThinWidth, 0, 1); + + $firstCharacter = true; + $textTable = str_split($this->getText()); + + // Characters + for ($i = 0; $i < $this->barcodeLength; $i++) { + if ($firstCharacter) { + $firstCharacter = false; + } else { + // Intermediate character (01) + $barcodeTable[] = array(0, $this->barThinWidth, 0, 1); + $barcodeTable[] = array(1, $this->barThinWidth, 0, 1); + } + $bars = str_split($this->codingMap[$this->getParity($i)][$textTable[$i]]); + foreach ($bars as $b) { + $barcodeTable[] = array($b, $this->barThinWidth, 0, 1); + } + } + + return $barcodeTable; + } + + /** + * Get barcode checksum + * + * @param string $text + * @return int + */ + public function getChecksum($text) + { + $this->checkText($text); + $checksum = 0; + + for ($i = 0; $i < $this->barcodeLength; $i ++) { + $checksum += intval($text{$i}) * ($i % 2 ? 9 : 3); + } + + return ($checksum % 10); + } + + /** + * @param int $i + * @return string + */ + protected function getParity($i) + { + $checksum = $this->getChecksum($this->getText()); + return $this->parities[$checksum][$i]; + } + + /** + * Retrieve text to encode + * @return string + */ + public function getText() + { + return $this->addLeadingZeros($this->text); + } +} diff --git a/library/Zend/Barcode/Object/Ean8.php b/library/Zend/Barcode/Object/Ean8.php new file mode 100755 index 0000000000..b36ec3e4c8 --- /dev/null +++ b/library/Zend/Barcode/Object/Ean8.php @@ -0,0 +1,146 @@ +barcodeLength = 8; + $this->mandatoryChecksum = true; + } + + /** + * Width of the barcode (in pixels) + * @return int + */ + protected function calculateBarcodeWidth() + { + $quietZone = $this->getQuietZone(); + $startCharacter = (3 * $this->barThinWidth) * $this->factor; + $middleCharacter = (5 * $this->barThinWidth) * $this->factor; + $stopCharacter = (3 * $this->barThinWidth) * $this->factor; + $encodedData = (7 * $this->barThinWidth) * $this->factor * 8; + return $quietZone + $startCharacter + $middleCharacter + $encodedData + $stopCharacter + $quietZone; + } + + /** + * Prepare array to draw barcode + * @return array + */ + protected function prepareBarcode() + { + $barcodeTable = array(); + $height = ($this->drawText) ? 1.1 : 1; + + // Start character (101) + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(0, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + + $textTable = str_split($this->getText()); + + // First part + for ($i = 0; $i < 4; $i++) { + $bars = str_split($this->codingMap['A'][$textTable[$i]]); + foreach ($bars as $b) { + $barcodeTable[] = array($b, $this->barThinWidth, 0, 1); + } + } + + // Middle character (01010) + $barcodeTable[] = array(0, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(0, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(0, $this->barThinWidth, 0, $height); + + // Second part + for ($i = 4; $i < 8; $i++) { + $bars = str_split($this->codingMap['C'][$textTable[$i]]); + foreach ($bars as $b) { + $barcodeTable[] = array($b, $this->barThinWidth, 0, 1); + } + } + + // Stop character (101) + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(0, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + return $barcodeTable; + } + + /** + * Partial function to draw text + * @return void + */ + protected function drawText() + { + if ($this->drawText) { + $text = $this->getTextToDisplay(); + $characterWidth = (7 * $this->barThinWidth) * $this->factor; + $leftPosition = $this->getQuietZone() + (3 * $this->barThinWidth) * $this->factor; + for ($i = 0; $i < $this->barcodeLength; $i ++) { + $this->addText( + $text{$i}, + $this->fontSize * $this->factor, + $this->rotate( + $leftPosition, + (int) $this->withBorder * 2 + $this->factor * ($this->barHeight + $this->fontSize) + 1 + ), + $this->font, + $this->foreColor, + 'left', + - $this->orientation + ); + switch ($i) { + case 3: + $factor = 4; + break; + default: + $factor = 0; + } + $leftPosition = $leftPosition + $characterWidth + ($factor * $this->barThinWidth * $this->factor); + } + } + } + + /** + * Particular validation for Ean8 barcode objects + * (to suppress checksum character substitution) + * + * @param string $value + * @param array $options + * @throws Exception\BarcodeValidationException + */ + protected function validateSpecificText($value, $options = array()) + { + $validator = new BarcodeValidator(array( + 'adapter' => 'ean8', + 'checksum' => false, + )); + + $value = $this->addLeadingZeros($value, true); + + if (!$validator->isValid($value)) { + $message = implode("\n", $validator->getMessages()); + throw new Exception\BarcodeValidationException($message); + } + } +} diff --git a/library/Zend/Barcode/Object/Error.php b/library/Zend/Barcode/Object/Error.php new file mode 100755 index 0000000000..15c36bcab2 --- /dev/null +++ b/library/Zend/Barcode/Object/Error.php @@ -0,0 +1,83 @@ +instructions = array(); + $this->addText('ERROR:', 10, array(5, 18), $this->font, 0, 'left'); + $this->addText($this->text, 10, array(5, 32), $this->font, 0, 'left'); + return $this->instructions; + } + + /** + * For compatibility reason + * @return void + */ + protected function prepareBarcode() + { + } + + /** + * For compatibility reason + * @return void + */ + protected function checkSpecificParams() + { + } + + /** + * For compatibility reason + * @return void + */ + protected function calculateBarcodeWidth() + { + } +} diff --git a/library/Zend/Barcode/Object/Exception/BarcodeValidationException.php b/library/Zend/Barcode/Object/Exception/BarcodeValidationException.php new file mode 100755 index 0000000000..64f3ea56e0 --- /dev/null +++ b/library/Zend/Barcode/Object/Exception/BarcodeValidationException.php @@ -0,0 +1,17 @@ +barcodeLength = 12; + $this->mandatoryChecksum = true; + } + + /** + * Retrieve text to display + * @return string + */ + public function getTextToDisplay() + { + return preg_replace('/([0-9]{2})([0-9]{3})([0-9]{3})([0-9]{3})([0-9])/', '$1.$2 $3.$4 $5', $this->getText()); + } + + /** + * Check allowed characters + * @param string $value + * @return string + * @throws Exception\BarcodeValidationException + */ + public function validateText($value) + { + $this->validateSpecificText($value, array('validator' => $this->getType())); + } + + /** + * Get barcode checksum + * + * @param string $text + * @return int + */ + public function getChecksum($text) + { + $this->checkText($text); + $checksum = 0; + + for ($i = strlen($text); $i > 0; $i --) { + $checksum += intval($text{$i - 1}) * (($i % 2) ? 4 : 9); + } + + $checksum = (10 - ($checksum % 10)) % 10; + + return $checksum; + } +} diff --git a/library/Zend/Barcode/Object/Itf14.php b/library/Zend/Barcode/Object/Itf14.php new file mode 100755 index 0000000000..361f9bd563 --- /dev/null +++ b/library/Zend/Barcode/Object/Itf14.php @@ -0,0 +1,26 @@ +barcodeLength = 14; + $this->mandatoryChecksum = true; + } +} diff --git a/library/Zend/Barcode/Object/Leitcode.php b/library/Zend/Barcode/Object/Leitcode.php new file mode 100755 index 0000000000..f82f3a5c03 --- /dev/null +++ b/library/Zend/Barcode/Object/Leitcode.php @@ -0,0 +1,35 @@ +barcodeLength = 14; + $this->mandatoryChecksum = true; + } + + /** + * Retrieve text to display + * @return string + */ + public function getTextToDisplay() + { + return preg_replace('/([0-9]{5})([0-9]{3})([0-9]{3})([0-9]{2})([0-9])/', '$1.$2.$3.$4 $5', $this->getText()); + } +} diff --git a/library/Zend/Barcode/Object/ObjectInterface.php b/library/Zend/Barcode/Object/ObjectInterface.php new file mode 100755 index 0000000000..836e0138ee --- /dev/null +++ b/library/Zend/Barcode/Object/ObjectInterface.php @@ -0,0 +1,337 @@ + "00111", + 1 => "11100", + 2 => "11010", + 3 => "11001", + 4 => "10110", + 5 => "10101", + 6 => "10011", + 7 => "01110", + 8 => "01101", + 9 => "01011" + ); +} diff --git a/library/Zend/Barcode/Object/Postnet.php b/library/Zend/Barcode/Object/Postnet.php new file mode 100755 index 0000000000..07b1d3c2d3 --- /dev/null +++ b/library/Zend/Barcode/Object/Postnet.php @@ -0,0 +1,110 @@ + "11000", + 1 => "00011", + 2 => "00101", + 3 => "00110", + 4 => "01001", + 5 => "01010", + 6 => "01100", + 7 => "10001", + 8 => "10010", + 9 => "10100" + ); + + /** + * Default options for Postnet barcode + * @return void + */ + protected function getDefaultOptions() + { + $this->barThinWidth = 2; + $this->barHeight = 20; + $this->drawText = false; + $this->stretchText = true; + $this->mandatoryChecksum = true; + } + + /** + * Width of the barcode (in pixels) + * @return int + */ + protected function calculateBarcodeWidth() + { + $quietZone = $this->getQuietZone(); + $startCharacter = (2 * $this->barThinWidth) * $this->factor; + $stopCharacter = (1 * $this->barThinWidth) * $this->factor; + $encodedData = (10 * $this->barThinWidth) * $this->factor * strlen($this->getText()); + return $quietZone + $startCharacter + $encodedData + $stopCharacter + $quietZone; + } + + /** + * Partial check of interleaved Postnet barcode + * @return void + */ + protected function checkSpecificParams() + { + } + + /** + * Prepare array to draw barcode + * @return array + */ + protected function prepareBarcode() + { + $barcodeTable = array(); + + // Start character (1) + $barcodeTable[] = array(1, $this->barThinWidth, 0, 1); + $barcodeTable[] = array(0, $this->barThinWidth, 0, 1); + + // Text to encode + $textTable = str_split($this->getText()); + foreach ($textTable as $char) { + $bars = str_split($this->codingMap[$char]); + foreach ($bars as $b) { + $barcodeTable[] = array(1, $this->barThinWidth, 0.5 - $b * 0.5, 1); + $barcodeTable[] = array(0, $this->barThinWidth, 0, 1); + } + } + + // Stop character (1) + $barcodeTable[] = array(1, $this->barThinWidth, 0, 1); + return $barcodeTable; + } + + /** + * Get barcode checksum + * + * @param string $text + * @return int + */ + public function getChecksum($text) + { + $this->checkText($text); + $sum = array_sum(str_split($text)); + $checksum = (10 - ($sum % 10)) % 10; + return $checksum; + } +} diff --git a/library/Zend/Barcode/Object/Royalmail.php b/library/Zend/Barcode/Object/Royalmail.php new file mode 100755 index 0000000000..23d8a709a7 --- /dev/null +++ b/library/Zend/Barcode/Object/Royalmail.php @@ -0,0 +1,137 @@ + '3300', '1' => '3210', '2' => '3201', '3' => '2310', '4' => '2301', '5' => '2211', + '6' => '3120', '7' => '3030', '8' => '3021', '9' => '2130', 'A' => '2121', 'B' => '2031', + 'C' => '3102', 'D' => '3012', 'E' => '3003', 'F' => '2112', 'G' => '2103', 'H' => '2013', + 'I' => '1320', 'J' => '1230', 'K' => '1221', 'L' => '0330', 'M' => '0321', 'N' => '0231', + 'O' => '1302', 'P' => '1212', 'Q' => '1203', 'R' => '0312', 'S' => '0303', 'T' => '0213', + 'U' => '1122', 'V' => '1032', 'W' => '1023', 'X' => '0132', 'Y' => '0123', 'Z' => '0033' + ); + + protected $rows = array( + '0' => 1, '1' => 1, '2' => 1, '3' => 1, '4' => 1, '5' => 1, + '6' => 2, '7' => 2, '8' => 2, '9' => 2, 'A' => 2, 'B' => 2, + 'C' => 3, 'D' => 3, 'E' => 3, 'F' => 3, 'G' => 3, 'H' => 3, + 'I' => 4, 'J' => 4, 'K' => 4, 'L' => 4, 'M' => 4, 'N' => 4, + 'O' => 5, 'P' => 5, 'Q' => 5, 'R' => 5, 'S' => 5, 'T' => 5, + 'U' => 0, 'V' => 0, 'W' => 0, 'X' => 0, 'Y' => 0, 'Z' => 0, + ); + + protected $columns = array( + '0' => 1, '1' => 2, '2' => 3, '3' => 4, '4' => 5, '5' => 0, + '6' => 1, '7' => 2, '8' => 3, '9' => 4, 'A' => 5, 'B' => 0, + 'C' => 1, 'D' => 2, 'E' => 3, 'F' => 4, 'G' => 5, 'H' => 0, + 'I' => 1, 'J' => 2, 'K' => 3, 'L' => 4, 'M' => 5, 'N' => 0, + 'O' => 1, 'P' => 2, 'Q' => 3, 'R' => 4, 'S' => 5, 'T' => 0, + 'U' => 1, 'V' => 2, 'W' => 3, 'X' => 4, 'Y' => 5, 'Z' => 0, + ); + + /** + * Default options for Postnet barcode + * @return void + */ + protected function getDefaultOptions() + { + $this->barThinWidth = 2; + $this->barHeight = 20; + $this->drawText = false; + $this->stretchText = true; + $this->mandatoryChecksum = true; + } + + /** + * Width of the barcode (in pixels) + * @return int + */ + protected function calculateBarcodeWidth() + { + $quietZone = $this->getQuietZone(); + $startCharacter = (2 * $this->barThinWidth) * $this->factor; + $stopCharacter = (1 * $this->barThinWidth) * $this->factor; + $encodedData = (8 * $this->barThinWidth) * $this->factor * strlen($this->getText()); + return $quietZone + $startCharacter + $encodedData + $stopCharacter + $quietZone; + } + + /** + * Partial check of interleaved Postnet barcode + * @return void + */ + protected function checkSpecificParams() + { + } + + /** + * Prepare array to draw barcode + * @return array + */ + protected function prepareBarcode() + { + $barcodeTable = array(); + + // Start character (1) + $barcodeTable[] = array(1, $this->barThinWidth, 0, 5/8); + $barcodeTable[] = array(0, $this->barThinWidth, 0, 1); + + // Text to encode + $textTable = str_split($this->getText()); + foreach ($textTable as $char) { + $bars = str_split($this->codingMap[$char]); + foreach ($bars as $b) { + $barcodeTable[] = array(1, $this->barThinWidth, ($b > 1 ? 3/8 : 0), ($b % 2 ? 5/8 : 1)); + $barcodeTable[] = array(0, $this->barThinWidth, 0, 1); + } + } + + // Stop character (1) + $barcodeTable[] = array(1, $this->barThinWidth, 0, 1); + return $barcodeTable; + } + + /** + * Get barcode checksum + * + * @param string $text + * @return int + */ + public function getChecksum($text) + { + $this->checkText($text); + $values = str_split($text); + $rowvalue = 0; + $colvalue = 0; + foreach ($values as $row) { + $rowvalue += $this->rows[$row]; + $colvalue += $this->columns[$row]; + } + + $rowvalue %= 6; + $colvalue %= 6; + + $rowchkvalue = array_keys($this->rows, $rowvalue); + $colchkvalue = array_keys($this->columns, $colvalue); + return current(array_intersect($rowchkvalue, $colchkvalue)); + } +} diff --git a/library/Zend/Barcode/Object/Upca.php b/library/Zend/Barcode/Object/Upca.php new file mode 100755 index 0000000000..1add66c781 --- /dev/null +++ b/library/Zend/Barcode/Object/Upca.php @@ -0,0 +1,144 @@ +barcodeLength = 12; + $this->mandatoryChecksum = true; + $this->mandatoryQuietZones = true; + } + + /** + * Width of the barcode (in pixels) + * @return int + */ + protected function calculateBarcodeWidth() + { + $quietZone = $this->getQuietZone(); + $startCharacter = (3 * $this->barThinWidth) * $this->factor; + $middleCharacter = (5 * $this->barThinWidth) * $this->factor; + $stopCharacter = (3 * $this->barThinWidth) * $this->factor; + $encodedData = (7 * $this->barThinWidth) * $this->factor * 12; + return $quietZone + $startCharacter + $middleCharacter + $encodedData + $stopCharacter + $quietZone; + } + + /** + * Prepare array to draw barcode + * @return array + */ + protected function prepareBarcode() + { + $barcodeTable = array(); + $height = ($this->drawText) ? 1.1 : 1; + + // Start character (101) + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(0, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + + $textTable = str_split($this->getText()); + + // First character + $bars = str_split($this->codingMap['A'][$textTable[0]]); + foreach ($bars as $b) { + $barcodeTable[] = array($b, $this->barThinWidth, 0, $height); + } + + // First part + for ($i = 1; $i < 6; $i++) { + $bars = str_split($this->codingMap['A'][$textTable[$i]]); + foreach ($bars as $b) { + $barcodeTable[] = array($b, $this->barThinWidth, 0, 1); + } + } + + // Middle character (01010) + $barcodeTable[] = array(0, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(0, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(0, $this->barThinWidth, 0, $height); + + // Second part + for ($i = 6; $i < 11; $i++) { + $bars = str_split($this->codingMap['C'][$textTable[$i]]); + foreach ($bars as $b) { + $barcodeTable[] = array($b, $this->barThinWidth, 0, 1); + } + } + + // Last character + $bars = str_split($this->codingMap['C'][$textTable[11]]); + foreach ($bars as $b) { + $barcodeTable[] = array($b, $this->barThinWidth, 0, $height); + } + + // Stop character (101) + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(0, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + return $barcodeTable; + } + + /** + * Partial function to draw text + * @return void + */ + protected function drawText() + { + if ($this->drawText) { + $text = $this->getTextToDisplay(); + $characterWidth = (7 * $this->barThinWidth) * $this->factor; + $leftPosition = $this->getQuietZone() - $characterWidth; + for ($i = 0; $i < $this->barcodeLength; $i ++) { + $fontSize = $this->fontSize; + if ($i == 0 || $i == 11) { + $fontSize *= 0.8; + } + $this->addText( + $text{$i}, + $fontSize * $this->factor, + $this->rotate( + $leftPosition, + (int) $this->withBorder * 2 + $this->factor * ($this->barHeight + $fontSize) + 1 + ), + $this->font, + $this->foreColor, + 'left', + - $this->orientation + ); + switch ($i) { + case 0: + $factor = 10; + break; + case 5: + $factor = 4; + break; + case 10: + $factor = 11; + break; + default: + $factor = 0; + } + $leftPosition = $leftPosition + $characterWidth + ($factor * $this->barThinWidth * $this->factor); + } + } + } +} diff --git a/library/Zend/Barcode/Object/Upce.php b/library/Zend/Barcode/Object/Upce.php new file mode 100755 index 0000000000..2114b11e30 --- /dev/null +++ b/library/Zend/Barcode/Object/Upce.php @@ -0,0 +1,199 @@ + array( + 0 => array('B','B','B','A','A','A'), + 1 => array('B','B','A','B','A','A'), + 2 => array('B','B','A','A','B','A'), + 3 => array('B','B','A','A','A','B'), + 4 => array('B','A','B','B','A','A'), + 5 => array('B','A','A','B','B','A'), + 6 => array('B','A','A','A','B','B'), + 7 => array('B','A','B','A','B','A'), + 8 => array('B','A','B','A','A','B'), + 9 => array('B','A','A','B','A','B')), + 1 => array( + 0 => array('A','A','A','B','B','B'), + 1 => array('A','A','B','A','B','B'), + 2 => array('A','A','B','B','A','B'), + 3 => array('A','A','B','B','B','A'), + 4 => array('A','B','A','A','B','B'), + 5 => array('A','B','B','A','A','B'), + 6 => array('A','B','B','B','A','A'), + 7 => array('A','B','A','B','A','B'), + 8 => array('A','B','A','B','B','A'), + 9 => array('A','B','B','A','B','A')) + ); + + /** + * Default options for Postnet barcode + * @return void + */ + protected function getDefaultOptions() + { + $this->barcodeLength = 8; + $this->mandatoryChecksum = true; + $this->mandatoryQuietZones = true; + } + + /** + * Retrieve text to encode + * @return string + */ + public function getText() + { + $text = parent::getText(); + if ($text[0] != 1) { + $text[0] = 0; + } + return $text; + } + + /** + * Width of the barcode (in pixels) + * @return int + */ + protected function calculateBarcodeWidth() + { + $quietZone = $this->getQuietZone(); + $startCharacter = (3 * $this->barThinWidth) * $this->factor; + $stopCharacter = (6 * $this->barThinWidth) * $this->factor; + $encodedData = (7 * $this->barThinWidth) * $this->factor * 6; + return $quietZone + $startCharacter + $encodedData + $stopCharacter + $quietZone; + } + + /** + * Prepare array to draw barcode + * @return array + */ + protected function prepareBarcode() + { + $barcodeTable = array(); + $height = ($this->drawText) ? 1.1 : 1; + + // Start character (101) + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(0, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + + $textTable = str_split($this->getText()); + $system = 0; + if ($textTable[0] == 1) { + $system = 1; + } + $checksum = $textTable[7]; + $parity = $this->parities[$system][$checksum]; + + for ($i = 1; $i < 7; $i++) { + $bars = str_split($this->codingMap[$parity[$i - 1]][$textTable[$i]]); + foreach ($bars as $b) { + $barcodeTable[] = array($b, $this->barThinWidth, 0, 1); + } + } + + // Stop character (10101) + $barcodeTable[] = array(0, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(0, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(0, $this->barThinWidth, 0, $height); + $barcodeTable[] = array(1, $this->barThinWidth, 0, $height); + return $barcodeTable; + } + + /** + * Partial function to draw text + * @return void + */ + protected function drawText() + { + if ($this->drawText) { + $text = $this->getTextToDisplay(); + $characterWidth = (7 * $this->barThinWidth) * $this->factor; + $leftPosition = $this->getQuietZone() - $characterWidth; + for ($i = 0; $i < $this->barcodeLength; $i ++) { + $fontSize = $this->fontSize; + if ($i == 0 || $i == 7) { + $fontSize *= 0.8; + } + $this->addText( + $text{$i}, + $fontSize * $this->factor, + $this->rotate( + $leftPosition, + (int) $this->withBorder * 2 + $this->factor * ($this->barHeight + $fontSize) + 1 + ), + $this->font, + $this->foreColor, + 'left', + - $this->orientation + ); + switch ($i) { + case 0: + $factor = 3; + break; + case 6: + $factor = 5; + break; + default: + $factor = 0; + } + $leftPosition = $leftPosition + $characterWidth + ($factor * $this->barThinWidth * $this->factor); + } + } + } + + /** + * Particular validation for Upce barcode objects + * (to suppress checksum character substitution) + * + * @param string $value + * @param array $options + * @throws Exception\BarcodeValidationException + */ + protected function validateSpecificText($value, $options = array()) + { + $validator = new BarcodeValidator(array( + 'adapter' => 'upce', + 'checksum' => false, + )); + + $value = $this->addLeadingZeros($value, true); + + if (!$validator->isValid($value)) { + $message = implode("\n", $validator->getMessages()); + throw new Exception\BarcodeValidationException($message); + } + } + + /** + * Get barcode checksum + * + * @param string $text + * @return int + */ + public function getChecksum($text) + { + $text = $this->addLeadingZeros($text, true); + if ($text[0] != 1) { + $text[0] = 0; + } + return parent::getChecksum($text); + } +} diff --git a/library/Zend/Barcode/ObjectPluginManager.php b/library/Zend/Barcode/ObjectPluginManager.php new file mode 100755 index 0000000000..40345f83bf --- /dev/null +++ b/library/Zend/Barcode/ObjectPluginManager.php @@ -0,0 +1,77 @@ + 'Zend\Barcode\Object\Codabar', + 'code128' => 'Zend\Barcode\Object\Code128', + 'code25' => 'Zend\Barcode\Object\Code25', + 'code25interleaved' => 'Zend\Barcode\Object\Code25interleaved', + 'code39' => 'Zend\Barcode\Object\Code39', + 'ean13' => 'Zend\Barcode\Object\Ean13', + 'ean2' => 'Zend\Barcode\Object\Ean2', + 'ean5' => 'Zend\Barcode\Object\Ean5', + 'ean8' => 'Zend\Barcode\Object\Ean8', + 'error' => 'Zend\Barcode\Object\Error', + 'identcode' => 'Zend\Barcode\Object\Identcode', + 'itf14' => 'Zend\Barcode\Object\Itf14', + 'leitcode' => 'Zend\Barcode\Object\Leitcode', + 'planet' => 'Zend\Barcode\Object\Planet', + 'postnet' => 'Zend\Barcode\Object\Postnet', + 'royalmail' => 'Zend\Barcode\Object\Royalmail', + 'upca' => 'Zend\Barcode\Object\Upca', + 'upce' => 'Zend\Barcode\Object\Upce', + ); + + /** + * Validate the plugin + * + * Checks that the barcode parser loaded is an instance + * of Object\AbstractObject. + * + * @param mixed $plugin + * @return void + * @throws Exception\InvalidArgumentException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof Object\AbstractObject) { + // we're okay + return; + } + + throw new Exception\InvalidArgumentException(sprintf( + 'Plugin of type %s is invalid; must extend %s\Object\AbstractObject', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/library/Zend/Barcode/README.md b/library/Zend/Barcode/README.md new file mode 100755 index 0000000000..2dc1c0895f --- /dev/null +++ b/library/Zend/Barcode/README.md @@ -0,0 +1,14 @@ +Barcode Component from ZF2 +========================== + +This is the Barcode component for ZF2. + +- File issues at https://github.com/zendframework/zf2/issues +- Create pull requests against https://github.com/zendframework/zf2 +- Documentation is at http://framework.zend.com/docs + +LICENSE +------- + +The files in this archive are released under the [Zend Framework +license](http://framework.zend.com/license), which is a 3-clause BSD license. diff --git a/library/Zend/Barcode/Renderer/AbstractRenderer.php b/library/Zend/Barcode/Renderer/AbstractRenderer.php new file mode 100755 index 0000000000..8a625e853b --- /dev/null +++ b/library/Zend/Barcode/Renderer/AbstractRenderer.php @@ -0,0 +1,515 @@ +setOptions($options); + } + $this->type = strtolower(substr( + get_class($this), + strlen($this->rendererNamespace) + 1 + )); + } + + /** + * Set renderer state from options array + * @param array $options + * @return AbstractRenderer + */ + public function setOptions($options) + { + foreach ($options as $key => $value) { + $method = 'set' . $key; + if (method_exists($this, $method)) { + $this->$method($value); + } + } + return $this; + } + + /** + * Set renderer namespace for autoloading + * + * @param string $namespace + * @return AbstractRenderer + */ + public function setRendererNamespace($namespace) + { + $this->rendererNamespace = $namespace; + return $this; + } + + /** + * Retrieve renderer namespace + * + * @return string + */ + public function getRendererNamespace() + { + return $this->rendererNamespace; + } + + /** + * Set whether background should be transparent + * Will work for SVG and Image (png and gif only) + * + * @param $bool + * @return $this + */ + public function setTransparentBackground($bool) + { + $this->transparentBackground = $bool; + + return $this; + } + + /** + * @return bool + */ + public function getTransparentBackground() + { + return $this->transparentBackground; + } + + /** + * Retrieve renderer type + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Manually adjust top position + * @param int $value + * @return AbstractRenderer + * @throws Exception\OutOfRangeException + */ + public function setTopOffset($value) + { + if (!is_numeric($value) || intval($value) < 0) { + throw new Exception\OutOfRangeException( + 'Vertical position must be greater than or equals 0' + ); + } + $this->topOffset = intval($value); + return $this; + } + + /** + * Retrieve vertical adjustment + * @return int + */ + public function getTopOffset() + { + return $this->topOffset; + } + + /** + * Manually adjust left position + * @param int $value + * @return AbstractRenderer + * @throws Exception\OutOfRangeException + */ + public function setLeftOffset($value) + { + if (!is_numeric($value) || intval($value) < 0) { + throw new Exception\OutOfRangeException( + 'Horizontal position must be greater than or equals 0' + ); + } + $this->leftOffset = intval($value); + return $this; + } + + /** + * Retrieve vertical adjustment + * @return int + */ + public function getLeftOffset() + { + return $this->leftOffset; + } + + /** + * Activate/Deactivate the automatic rendering of exception + * @param bool $value + * @return AbstractRenderer + */ + public function setAutomaticRenderError($value) + { + $this->automaticRenderError = (bool) $value; + return $this; + } + + /** + * Horizontal position of the barcode in the rendering resource + * @param string $value + * @return AbstractRenderer + * @throws Exception\UnexpectedValueException + */ + public function setHorizontalPosition($value) + { + if (!in_array($value, array('left', 'center', 'right'))) { + throw new Exception\UnexpectedValueException( + "Invalid barcode position provided must be 'left', 'center' or 'right'" + ); + } + $this->horizontalPosition = $value; + return $this; + } + + /** + * Horizontal position of the barcode in the rendering resource + * @return string + */ + public function getHorizontalPosition() + { + return $this->horizontalPosition; + } + + /** + * Vertical position of the barcode in the rendering resource + * @param string $value + * @return AbstractRenderer + * @throws Exception\UnexpectedValueException + */ + public function setVerticalPosition($value) + { + if (!in_array($value, array('top', 'middle', 'bottom'))) { + throw new Exception\UnexpectedValueException( + "Invalid barcode position provided must be 'top', 'middle' or 'bottom'" + ); + } + $this->verticalPosition = $value; + return $this; + } + + /** + * Vertical position of the barcode in the rendering resource + * @return string + */ + public function getVerticalPosition() + { + return $this->verticalPosition; + } + + /** + * Set the size of a module + * @param float $value + * @return AbstractRenderer + * @throws Exception\OutOfRangeException + */ + public function setModuleSize($value) + { + if (!is_numeric($value) || floatval($value) <= 0) { + throw new Exception\OutOfRangeException( + 'Float size must be greater than 0' + ); + } + $this->moduleSize = floatval($value); + return $this; + } + + /** + * Set the size of a module + * @return float + */ + public function getModuleSize() + { + return $this->moduleSize; + } + + /** + * Retrieve the automatic rendering of exception + * @return bool + */ + public function getAutomaticRenderError() + { + return $this->automaticRenderError; + } + + /** + * Set the barcode object + * @param Object\ObjectInterface $barcode + * @return AbstractRenderer + */ + public function setBarcode(Object\ObjectInterface $barcode) + { + $this->barcode = $barcode; + return $this; + } + + /** + * Retrieve the barcode object + * @return Object\ObjectInterface + */ + public function getBarcode() + { + return $this->barcode; + } + + /** + * Checking of parameters after all settings + * @return bool + */ + public function checkParams() + { + $this->checkBarcodeObject(); + $this->checkSpecificParams(); + return true; + } + + /** + * Check if a barcode object is correctly provided + * @return void + * @throws Exception\RuntimeException + */ + protected function checkBarcodeObject() + { + if ($this->barcode === null) { + throw new Exception\RuntimeException( + 'No barcode object provided' + ); + } + } + + /** + * Calculate the left and top offset of the barcode in the + * rendering support + * + * @param float $supportHeight + * @param float $supportWidth + * @return void + */ + protected function adjustPosition($supportHeight, $supportWidth) + { + $barcodeHeight = $this->barcode->getHeight(true) * $this->moduleSize; + if ($barcodeHeight != $supportHeight && $this->topOffset == 0) { + switch ($this->verticalPosition) { + case 'middle': + $this->topOffset = floor(($supportHeight - $barcodeHeight) / 2); + break; + case 'bottom': + $this->topOffset = $supportHeight - $barcodeHeight; + break; + case 'top': + default: + $this->topOffset = 0; + break; + } + } + $barcodeWidth = $this->barcode->getWidth(true) * $this->moduleSize; + if ($barcodeWidth != $supportWidth && $this->leftOffset == 0) { + switch ($this->horizontalPosition) { + case 'center': + $this->leftOffset = floor(($supportWidth - $barcodeWidth) / 2); + break; + case 'right': + $this->leftOffset = $supportWidth - $barcodeWidth; + break; + case 'left': + default: + $this->leftOffset = 0; + break; + } + } + } + + /** + * Draw the barcode in the rendering resource + * + * @throws BarcodeException\ExceptionInterface + * @return mixed + */ + public function draw() + { + try { + $this->checkParams(); + $this->initRenderer(); + $this->drawInstructionList(); + } catch (BarcodeException\ExceptionInterface $e) { + if ($this->automaticRenderError && !($e instanceof BarcodeException\RendererCreationException)) { + $barcode = Barcode::makeBarcode( + 'error', + array('text' => $e->getMessage()) + ); + $this->setBarcode($barcode); + $this->resource = null; + $this->initRenderer(); + $this->drawInstructionList(); + } else { + throw $e; + } + } + return $this->resource; + } + + /** + * Sub process to draw the barcode instructions + * Needed by the automatic error rendering + */ + private function drawInstructionList() + { + $instructionList = $this->barcode->draw(); + foreach ($instructionList as $instruction) { + switch ($instruction['type']) { + case 'polygon': + $this->drawPolygon( + $instruction['points'], + $instruction['color'], + $instruction['filled'] + ); + break; + case 'text': //$text, $size, $position, $font, $color, $alignment = 'center', $orientation = 0) + $this->drawText( + $instruction['text'], + $instruction['size'], + $instruction['position'], + $instruction['font'], + $instruction['color'], + $instruction['alignment'], + $instruction['orientation'] + ); + break; + default: + throw new Exception\UnexpectedValueException( + 'Unkown drawing command' + ); + } + } + } + + /** + * Checking of parameters after all settings + * @return void + */ + abstract protected function checkSpecificParams(); + + /** + * Initialize the rendering resource + * @return void + */ + abstract protected function initRenderer(); + + /** + * Draw a polygon in the rendering resource + * @param array $points + * @param int $color + * @param bool $filled + */ + abstract protected function drawPolygon($points, $color, $filled = true); + + /** + * Draw a polygon in the rendering resource + * @param string $text + * @param float $size + * @param array $position + * @param string $font + * @param int $color + * @param string $alignment + * @param float $orientation + */ + abstract protected function drawText( + $text, + $size, + $position, + $font, + $color, + $alignment = 'center', + $orientation = 0 + ); +} diff --git a/library/Zend/Barcode/Renderer/Exception/ExceptionInterface.php b/library/Zend/Barcode/Renderer/Exception/ExceptionInterface.php new file mode 100755 index 0000000000..f7f4d44547 --- /dev/null +++ b/library/Zend/Barcode/Renderer/Exception/ExceptionInterface.php @@ -0,0 +1,16 @@ +userHeight = intval($value); + return $this; + } + + /** + * Get barcode height + * + * @return int + */ + public function getHeight() + { + return $this->userHeight; + } + + /** + * Set barcode width + * + * @param mixed $value + * @throws Exception\OutOfRangeException + * @return self + */ + public function setWidth($value) + { + if (!is_numeric($value) || intval($value) < 0) { + throw new Exception\OutOfRangeException( + 'Image width must be greater than or equals 0' + ); + } + $this->userWidth = intval($value); + return $this; + } + + /** + * Get barcode width + * + * @return int + */ + public function getWidth() + { + return $this->userWidth; + } + + /** + * Set an image resource to draw the barcode inside + * + * @param resource $image + * @return Image + * @throws Exception\InvalidArgumentException + */ + public function setResource($image) + { + if (gettype($image) != 'resource' || get_resource_type($image) != 'gd') { + throw new Exception\InvalidArgumentException( + 'Invalid image resource provided to setResource()' + ); + } + $this->resource = $image; + return $this; + } + + /** + * Set the image type to produce (png, jpeg, gif) + * + * @param string $value + * @throws Exception\InvalidArgumentException + * @return Image + */ + public function setImageType($value) + { + if ($value == 'jpg') { + $value = 'jpeg'; + } + + if (!in_array($value, $this->allowedImageType)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid type "%s" provided to setImageType()', + $value + )); + } + + $this->imageType = $value; + return $this; + } + + /** + * Retrieve the image type to produce + * + * @return string + */ + public function getImageType() + { + return $this->imageType; + } + + /** + * Initialize the image resource + * + * @return void + */ + protected function initRenderer() + { + $barcodeWidth = $this->barcode->getWidth(true); + $barcodeHeight = $this->barcode->getHeight(true); + + if (null === $this->resource) { + $width = $barcodeWidth; + $height = $barcodeHeight; + if ($this->userWidth && $this->barcode->getType() != 'error') { + $width = $this->userWidth; + } + if ($this->userHeight && $this->barcode->getType() != 'error') { + $height = $this->userHeight; + } + + $this->resource = imagecreatetruecolor($width, $height); + + $white = imagecolorallocate($this->resource, 255, 255, 255); + imagefilledrectangle($this->resource, 0, 0, $width - 1, $height - 1, $white); + } + + $foreColor = $this->barcode->getForeColor(); + $this->imageForeColor = imagecolorallocate( + $this->resource, + ($foreColor & 0xFF0000) >> 16, + ($foreColor & 0x00FF00) >> 8, + $foreColor & 0x0000FF + ); + + $backgroundColor = $this->barcode->getBackgroundColor(); + $this->imageBackgroundColor = imagecolorallocate( + $this->resource, + ($backgroundColor & 0xFF0000) >> 16, + ($backgroundColor & 0x00FF00) >> 8, + $backgroundColor & 0x0000FF + ); + + // JPEG does not support transparency, if transparentBackground is true and + // image type is JPEG, ignore transparency + if ($this->getImageType() != "jpeg" && $this->transparentBackground) { + imagecolortransparent($this->resource, $this->imageBackgroundColor); + } + + $this->adjustPosition(imagesy($this->resource), imagesx($this->resource)); + + imagefilledrectangle( + $this->resource, + $this->leftOffset, + $this->topOffset, + $this->leftOffset + $barcodeWidth - 1, + $this->topOffset + $barcodeHeight - 1, + $this->imageBackgroundColor + ); + } + + /** + * Check barcode parameters + * + * @return void + */ + protected function checkSpecificParams() + { + $this->checkDimensions(); + } + + /** + * Check barcode dimensions + * + * @throws Exception\RuntimeException + * @return void + */ + protected function checkDimensions() + { + if ($this->resource !== null) { + if (imagesy($this->resource) < $this->barcode->getHeight(true)) { + throw new Exception\RuntimeException( + 'Barcode is define outside the image (height)' + ); + } + } else { + if ($this->userHeight) { + $height = $this->barcode->getHeight(true); + if ($this->userHeight < $height) { + throw new Exception\RuntimeException(sprintf( + "Barcode is define outside the image (calculated: '%d', provided: '%d')", + $height, + $this->userHeight + )); + } + } + } + if ($this->resource !== null) { + if (imagesx($this->resource) < $this->barcode->getWidth(true)) { + throw new Exception\RuntimeException( + 'Barcode is define outside the image (width)' + ); + } + } else { + if ($this->userWidth) { + $width = $this->barcode->getWidth(true); + if ($this->userWidth < $width) { + throw new Exception\RuntimeException(sprintf( + "Barcode is define outside the image (calculated: '%d', provided: '%d')", + $width, + $this->userWidth + )); + } + } + } + } + + /** + * Draw and render the barcode with correct headers + * + * @return mixed + */ + public function render() + { + $this->draw(); + header("Content-Type: image/" . $this->imageType); + $functionName = 'image' . $this->imageType; + $functionName($this->resource); + + ErrorHandler::start(E_WARNING); + imagedestroy($this->resource); + ErrorHandler::stop(); + } + + /** + * Draw a polygon in the image resource + * + * @param array $points + * @param int $color + * @param bool $filled + */ + protected function drawPolygon($points, $color, $filled = true) + { + $newPoints = array($points[0][0] + $this->leftOffset, + $points[0][1] + $this->topOffset, + $points[1][0] + $this->leftOffset, + $points[1][1] + $this->topOffset, + $points[2][0] + $this->leftOffset, + $points[2][1] + $this->topOffset, + $points[3][0] + $this->leftOffset, + $points[3][1] + $this->topOffset, ); + + $allocatedColor = imagecolorallocate( + $this->resource, + ($color & 0xFF0000) >> 16, + ($color & 0x00FF00) >> 8, + $color & 0x0000FF + ); + + if ($filled) { + imagefilledpolygon($this->resource, $newPoints, 4, $allocatedColor); + } else { + imagepolygon($this->resource, $newPoints, 4, $allocatedColor); + } + } + + /** + * Draw a polygon in the image resource + * + * @param string $text + * @param float $size + * @param array $position + * @param string $font + * @param int $color + * @param string $alignment + * @param float $orientation + * @throws Exception\RuntimeException + */ + protected function drawText($text, $size, $position, $font, $color, $alignment = 'center', $orientation = 0) + { + $allocatedColor = imagecolorallocate( + $this->resource, + ($color & 0xFF0000) >> 16, + ($color & 0x00FF00) >> 8, + $color & 0x0000FF + ); + + if ($font == null) { + $font = 3; + } + $position[0] += $this->leftOffset; + $position[1] += $this->topOffset; + + if (is_numeric($font)) { + if ($orientation) { + /** + * imagestring() doesn't allow orientation, if orientation + * needed: a TTF font is required. + * Throwing an exception here, allow to use automaticRenderError + * to informe user of the problem instead of simply not drawing + * the text + */ + throw new Exception\RuntimeException( + 'No orientation possible with GD internal font' + ); + } + $fontWidth = imagefontwidth($font); + $positionY = $position[1] - imagefontheight($font) + 1; + switch ($alignment) { + case 'left': + $positionX = $position[0]; + break; + case 'center': + $positionX = $position[0] - ceil(($fontWidth * strlen($text)) / 2); + break; + case 'right': + $positionX = $position[0] - ($fontWidth * strlen($text)); + break; + } + imagestring($this->resource, $font, $positionX, $positionY, $text, $color); + } else { + if (!function_exists('imagettfbbox')) { + throw new Exception\RuntimeException( + 'A font was provided, but this instance of PHP does not have TTF (FreeType) support' + ); + } + + $box = imagettfbbox($size, 0, $font, $text); + switch ($alignment) { + case 'left': + $width = 0; + break; + case 'center': + $width = ($box[2] - $box[0]) / 2; + break; + case 'right': + $width = ($box[2] - $box[0]); + break; + } + imagettftext( + $this->resource, + $size, + $orientation, + $position[0] - ($width * cos(pi() * $orientation / 180)), + $position[1] + ($width * sin(pi() * $orientation / 180)), + $allocatedColor, + $font, + $text + ); + } + } +} diff --git a/library/Zend/Barcode/Renderer/Pdf.php b/library/Zend/Barcode/Renderer/Pdf.php new file mode 100755 index 0000000000..e4e084d491 --- /dev/null +++ b/library/Zend/Barcode/Renderer/Pdf.php @@ -0,0 +1,215 @@ +resource = $pdf; + $this->page = intval($page); + + if (!count($this->resource->pages)) { + $this->page = 0; + $this->resource->pages[] = new Page( + Page::SIZE_A4 + ); + } + return $this; + } + + /** + * Check renderer parameters + * + * @return void + */ + protected function checkSpecificParams() + { + } + + /** + * Draw the barcode in the PDF, send headers and the PDF + * @return mixed + */ + public function render() + { + $this->draw(); + header("Content-Type: application/pdf"); + echo $this->resource->render(); + } + + /** + * Initialize the PDF resource + * @return void + */ + protected function initRenderer() + { + if ($this->resource === null) { + $this->resource = new PdfDocument(); + $this->resource->pages[] = new Page( + Page::SIZE_A4 + ); + } + + $pdfPage = $this->resource->pages[$this->page]; + $this->adjustPosition($pdfPage->getHeight(), $pdfPage->getWidth()); + } + + /** + * Draw a polygon in the rendering resource + * @param array $points + * @param int $color + * @param bool $filled + */ + protected function drawPolygon($points, $color, $filled = true) + { + $page = $this->resource->pages[$this->page]; + $x = array(); + $y = array(); + foreach ($points as $point) { + $x[] = $point[0] * $this->moduleSize + $this->leftOffset; + $y[] = $page->getHeight() - $point[1] * $this->moduleSize - $this->topOffset; + } + if (count($y) == 4) { + if ($x[0] != $x[3] && $y[0] == $y[3]) { + $y[0] -= ($this->moduleSize / 2); + $y[3] -= ($this->moduleSize / 2); + } + if ($x[1] != $x[2] && $y[1] == $y[2]) { + $y[1] += ($this->moduleSize / 2); + $y[2] += ($this->moduleSize / 2); + } + } + + $color = new Color\Rgb( + (($color & 0xFF0000) >> 16) / 255.0, + (($color & 0x00FF00) >> 8) / 255.0, + ($color & 0x0000FF) / 255.0 + ); + + $page->setLineColor($color); + $page->setFillColor($color); + $page->setLineWidth($this->moduleSize); + + $fillType = ($filled) + ? Page::SHAPE_DRAW_FILL_AND_STROKE + : Page::SHAPE_DRAW_STROKE; + + $page->drawPolygon($x, $y, $fillType); + } + + /** + * Draw a polygon in the rendering resource + * @param string $text + * @param float $size + * @param array $position + * @param string $font + * @param int $color + * @param string $alignment + * @param float $orientation + */ + protected function drawText( + $text, + $size, + $position, + $font, + $color, + $alignment = 'center', + $orientation = 0 + ) { + $page = $this->resource->pages[$this->page]; + $color = new Color\Rgb( + (($color & 0xFF0000) >> 16) / 255.0, + (($color & 0x00FF00) >> 8) / 255.0, + ($color & 0x0000FF) / 255.0 + ); + + $page->setLineColor($color); + $page->setFillColor($color); + $page->setFont(Font::fontWithPath($font), $size * $this->moduleSize * 1.2); + + $width = $this->widthForStringUsingFontSize( + $text, + Font::fontWithPath($font), + $size * $this->moduleSize + ); + + $angle = pi() * $orientation / 180; + $left = $position[0] * $this->moduleSize + $this->leftOffset; + $top = $page->getHeight() - $position[1] * $this->moduleSize - $this->topOffset; + + switch ($alignment) { + case 'center': + $left -= ($width / 2) * cos($angle); + $top -= ($width / 2) * sin($angle); + break; + case 'right': + $left -= $width; + break; + } + $page->rotate($left, $top, $angle); + $page->drawText($text, $left, $top); + $page->rotate($left, $top, - $angle); + } + + /** + * Calculate the width of a string: + * in case of using alignment parameter in drawText + * @param string $text + * @param Font $font + * @param float $fontSize + * @return float + */ + public function widthForStringUsingFontSize($text, $font, $fontSize) + { + $drawingString = iconv('UTF-8', 'UTF-16BE//IGNORE', $text); + $characters = array(); + for ($i = 0, $len = strlen($drawingString); $i < $len; $i++) { + $characters[] = (ord($drawingString[$i ++]) << 8) | ord($drawingString[$i]); + } + $glyphs = $font->glyphNumbersForCharacters($characters); + $widths = $font->widthsForGlyphs($glyphs); + $stringWidth = (array_sum($widths) / $font->getUnitsPerEm()) * $fontSize; + return $stringWidth; + } +} diff --git a/library/Zend/Barcode/Renderer/RendererInterface.php b/library/Zend/Barcode/Renderer/RendererInterface.php new file mode 100755 index 0000000000..795654f303 --- /dev/null +++ b/library/Zend/Barcode/Renderer/RendererInterface.php @@ -0,0 +1,161 @@ +userHeight = intval($value); + return $this; + } + + /** + * Get barcode height + * + * @return int + */ + public function getHeight() + { + return $this->userHeight; + } + + /** + * Set barcode width + * + * @param mixed $value + * @throws Exception\OutOfRangeException + * @return self + */ + public function setWidth($value) + { + if (!is_numeric($value) || intval($value) < 0) { + throw new Exception\OutOfRangeException( + 'Svg width must be greater than or equals 0' + ); + } + $this->userWidth = intval($value); + return $this; + } + + /** + * Get barcode width + * + * @return int + */ + public function getWidth() + { + return $this->userWidth; + } + + /** + * Set an image resource to draw the barcode inside + * + * @param DOMDocument $svg + * @return Svg + */ + public function setResource(DOMDocument $svg) + { + $this->resource = $svg; + return $this; + } + + /** + * Initialize the image resource + * + * @return void + */ + protected function initRenderer() + { + $barcodeWidth = $this->barcode->getWidth(true); + $barcodeHeight = $this->barcode->getHeight(true); + + $backgroundColor = $this->barcode->getBackgroundColor(); + $imageBackgroundColor = 'rgb(' . implode(', ', array(($backgroundColor & 0xFF0000) >> 16, + ($backgroundColor & 0x00FF00) >> 8, + ($backgroundColor & 0x0000FF))) . ')'; + + $width = $barcodeWidth; + $height = $barcodeHeight; + if ($this->userWidth && $this->barcode->getType() != 'error') { + $width = $this->userWidth; + } + if ($this->userHeight && $this->barcode->getType() != 'error') { + $height = $this->userHeight; + } + if ($this->resource === null) { + $this->resource = new DOMDocument('1.0', 'utf-8'); + $this->resource->formatOutput = true; + $this->rootElement = $this->resource->createElement('svg'); + $this->rootElement->setAttribute('xmlns', "http://www.w3.org/2000/svg"); + $this->rootElement->setAttribute('version', '1.1'); + $this->rootElement->setAttribute('width', $width); + $this->rootElement->setAttribute('height', $height); + + $this->appendRootElement( + 'title', + array(), + "Barcode " . strtoupper($this->barcode->getType()) . " " . $this->barcode->getText() + ); + } else { + $this->readRootElement(); + $width = $this->rootElement->getAttribute('width'); + $height = $this->rootElement->getAttribute('height'); + } + $this->adjustPosition($height, $width); + + $rect = array('x' => $this->leftOffset, + 'y' => $this->topOffset, + 'width' => ($this->leftOffset + $barcodeWidth - 1), + 'height' => ($this->topOffset + $barcodeHeight - 1), + 'fill' => $imageBackgroundColor); + + if ($this->transparentBackground) { + $rect['fill-opacity'] = 0; + } + + $this->appendRootElement('rect', $rect); + } + + protected function readRootElement() + { + if ($this->resource !== null) { + $this->rootElement = $this->resource->documentElement; + } + } + + /** + * Append a new DOMElement to the root element + * + * @param string $tagName + * @param array $attributes + * @param string $textContent + */ + protected function appendRootElement($tagName, $attributes = array(), $textContent = null) + { + $newElement = $this->createElement($tagName, $attributes, $textContent); + $this->rootElement->appendChild($newElement); + } + + /** + * Create DOMElement + * + * @param string $tagName + * @param array $attributes + * @param string $textContent + * @return DOMElement + */ + protected function createElement($tagName, $attributes = array(), $textContent = null) + { + $element = $this->resource->createElement($tagName); + foreach ($attributes as $k => $v) { + $element->setAttribute($k, $v); + } + if ($textContent !== null) { + $element->appendChild(new DOMText((string) $textContent)); + } + return $element; + } + + /** + * Check barcode parameters + * + * @return void + */ + protected function checkSpecificParams() + { + $this->checkDimensions(); + } + + /** + * Check barcode dimensions + * + * @throws Exception\RuntimeException + * @return void + */ + protected function checkDimensions() + { + if ($this->resource !== null) { + $this->readRootElement(); + $height = (float) $this->rootElement->getAttribute('height'); + if ($height < $this->barcode->getHeight(true)) { + throw new Exception\RuntimeException( + 'Barcode is define outside the image (height)' + ); + } + } else { + if ($this->userHeight) { + $height = $this->barcode->getHeight(true); + if ($this->userHeight < $height) { + throw new Exception\RuntimeException(sprintf( + "Barcode is define outside the image (calculated: '%d', provided: '%d')", + $height, + $this->userHeight + )); + } + } + } + if ($this->resource !== null) { + $this->readRootElement(); + $width = $this->rootElement->getAttribute('width'); + if ($width < $this->barcode->getWidth(true)) { + throw new Exception\RuntimeException( + 'Barcode is define outside the image (width)' + ); + } + } else { + if ($this->userWidth) { + $width = (float) $this->barcode->getWidth(true); + if ($this->userWidth < $width) { + throw new Exception\RuntimeException(sprintf( + "Barcode is define outside the image (calculated: '%d', provided: '%d')", + $width, + $this->userWidth + )); + } + } + } + } + + /** + * Draw the barcode in the rendering resource + * @return DOMDocument + */ + public function draw() + { + parent::draw(); + $this->resource->appendChild($this->rootElement); + return $this->resource; + } + + /** + * Draw and render the barcode with correct headers + * + * @return mixed + */ + public function render() + { + $this->draw(); + header("Content-Type: image/svg+xml"); + echo $this->resource->saveXML(); + } + + /** + * Draw a polygon in the svg resource + * + * @param array $points + * @param int $color + * @param bool $filled + */ + protected function drawPolygon($points, $color, $filled = true) + { + $color = 'rgb(' . implode(', ', array(($color & 0xFF0000) >> 16, + ($color & 0x00FF00) >> 8, + ($color & 0x0000FF))) . ')'; + $orientation = $this->getBarcode()->getOrientation(); + $newPoints = array( + $points[0][0] + $this->leftOffset, + $points[0][1] + $this->topOffset, + $points[1][0] + $this->leftOffset, + $points[1][1] + $this->topOffset, + $points[2][0] + $this->leftOffset + cos(-$orientation), + $points[2][1] + $this->topOffset - sin($orientation), + $points[3][0] + $this->leftOffset + cos(-$orientation), + $points[3][1] + $this->topOffset - sin($orientation), + ); + $newPoints = implode(' ', $newPoints); + $attributes = array(); + $attributes['points'] = $newPoints; + $attributes['fill'] = $color; + + // SVG passes a rect in as the first call to drawPolygon, we'll need to intercept + // this and set transparency if necessary. + if (!$this->drawPolygonExecuted) { + if ($this->transparentBackground) { + $attributes['fill-opacity'] = '0'; + } + $this->drawPolygonExecuted = true; + } + + $this->appendRootElement('polygon', $attributes); + } + + /** + * Draw a polygon in the svg resource + * + * @param string $text + * @param float $size + * @param array $position + * @param string $font + * @param int $color + * @param string $alignment + * @param float $orientation + */ + protected function drawText($text, $size, $position, $font, $color, $alignment = 'center', $orientation = 0) + { + $color = 'rgb(' . implode(', ', array(($color & 0xFF0000) >> 16, + ($color & 0x00FF00) >> 8, + ($color & 0x0000FF))) . ')'; + $attributes = array(); + $attributes['x'] = $position[0] + $this->leftOffset; + $attributes['y'] = $position[1] + $this->topOffset; + //$attributes['font-family'] = $font; + $attributes['color'] = $color; + $attributes['font-size'] = $size * 1.2; + switch ($alignment) { + case 'left': + $textAnchor = 'start'; + break; + case 'right': + $textAnchor = 'end'; + break; + case 'center': + default: + $textAnchor = 'middle'; + } + $attributes['style'] = 'text-anchor: ' . $textAnchor; + $attributes['transform'] = 'rotate(' + . (- $orientation) + . ', ' + . ($position[0] + $this->leftOffset) + . ', ' . ($position[1] + $this->topOffset) + . ')'; + $this->appendRootElement('text', $attributes, $text); + } +} diff --git a/library/Zend/Barcode/RendererPluginManager.php b/library/Zend/Barcode/RendererPluginManager.php new file mode 100755 index 0000000000..0c8c2f8aee --- /dev/null +++ b/library/Zend/Barcode/RendererPluginManager.php @@ -0,0 +1,62 @@ + 'Zend\Barcode\Renderer\Image', + 'pdf' => 'Zend\Barcode\Renderer\Pdf', + 'svg' => 'Zend\Barcode\Renderer\Svg' + ); + + /** + * Validate the plugin + * + * Checks that the barcode parser loaded is an instance + * of Renderer\AbstractRenderer. + * + * @param mixed $plugin + * @return void + * @throws Exception\InvalidArgumentException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof Renderer\AbstractRenderer) { + // we're okay + return; + } + + throw new Exception\InvalidArgumentException(sprintf( + 'Plugin of type %s is invalid; must extend %s\Renderer\AbstractRenderer', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/library/Zend/Barcode/composer.json b/library/Zend/Barcode/composer.json new file mode 100755 index 0000000000..f351302ebb --- /dev/null +++ b/library/Zend/Barcode/composer.json @@ -0,0 +1,35 @@ +{ + "name": "zendframework/zend-barcode", + "description": "provides a generic way to generate barcodes", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "barcode" + ], + "homepage": "https://github.com/zendframework/zf2", + "autoload": { + "psr-0": { + "Zend\\Barcode\\": "" + } + }, + "target-dir": "Zend/Barcode", + "require": { + "php": ">=5.3.23", + "zendframework/zend-stdlib": "self.version", + "zendframework/zend-validator": "self.version" + }, + "require-dev": { + "zendframework/zend-servicemanager": "self.version", + "zendframework/zendpdf": "*" + }, + "suggest": { + "zendframework/zend-servicemanager": "Zend\\ServiceManager component, required when using the factory methods of Zend\\Barcode.", + "zendframework/zendpdf": "ZendPdf component" + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev", + "dev-develop": "2.4-dev" + } + } +} diff --git a/library/Zend/Cache/CONTRIBUTING.md b/library/Zend/Cache/CONTRIBUTING.md new file mode 100755 index 0000000000..e77f5d2d5b --- /dev/null +++ b/library/Zend/Cache/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# CONTRIBUTING + +Please don't open pull requests against this repository, please use https://github.com/zendframework/zf2. \ No newline at end of file diff --git a/library/Zend/Cache/Exception/BadMethodCallException.php b/library/Zend/Cache/Exception/BadMethodCallException.php new file mode 100755 index 0000000000..dc8af1fc5f --- /dev/null +++ b/library/Zend/Cache/Exception/BadMethodCallException.php @@ -0,0 +1,15 @@ +options = $options; + return $this; + } + + /** + * Get all pattern options + * + * @return PatternOptions + */ + public function getOptions() + { + if (null === $this->options) { + $this->setOptions(new PatternOptions()); + } + return $this->options; + } +} diff --git a/library/Zend/Cache/Pattern/CallbackCache.php b/library/Zend/Cache/Pattern/CallbackCache.php new file mode 100755 index 0000000000..a26a00746a --- /dev/null +++ b/library/Zend/Cache/Pattern/CallbackCache.php @@ -0,0 +1,200 @@ +getStorage()) { + throw new Exception\InvalidArgumentException("Missing option 'storage'"); + } + return $this; + } + + /** + * Call the specified callback or get the result from cache + * + * @param callable $callback A valid callback + * @param array $args Callback arguments + * @return mixed Result + * @throws Exception\RuntimeException if invalid cached data + * @throws \Exception + */ + public function call($callback, array $args = array()) + { + $options = $this->getOptions(); + $storage = $options->getStorage(); + $success = null; + $key = $this->generateCallbackKey($callback, $args); + $result = $storage->getItem($key, $success); + if ($success) { + if (!array_key_exists(0, $result)) { + throw new Exception\RuntimeException("Invalid cached data for key '{$key}'"); + } + + echo isset($result[1]) ? $result[1] : ''; + return $result[0]; + } + + $cacheOutput = $options->getCacheOutput(); + if ($cacheOutput) { + ob_start(); + ob_implicit_flush(false); + } + + // TODO: do not cache on errors using [set|restore]_error_handler + + try { + if ($args) { + $ret = call_user_func_array($callback, $args); + } else { + $ret = call_user_func($callback); + } + } catch (\Exception $e) { + if ($cacheOutput) { + ob_end_flush(); + } + throw $e; + } + + if ($cacheOutput) { + $data = array($ret, ob_get_flush()); + } else { + $data = array($ret); + } + + $storage->setItem($key, $data); + + return $ret; + } + + /** + * function call handler + * + * @param string $function Function name to call + * @param array $args Function arguments + * @return mixed + * @throws Exception\RuntimeException + * @throws \Exception + */ + public function __call($function, array $args) + { + return $this->call($function, $args); + } + + /** + * Generate a unique key in base of a key representing the callback part + * and a key representing the arguments part. + * + * @param callable $callback A valid callback + * @param array $args Callback arguments + * @return string + * @throws Exception\RuntimeException + * @throws Exception\InvalidArgumentException + */ + public function generateKey($callback, array $args = array()) + { + return $this->generateCallbackKey($callback, $args); + } + + /** + * Generate a unique key in base of a key representing the callback part + * and a key representing the arguments part. + * + * @param callable $callback A valid callback + * @param array $args Callback arguments + * @throws Exception\RuntimeException if callback not serializable + * @throws Exception\InvalidArgumentException if invalid callback + * @return string + */ + protected function generateCallbackKey($callback, array $args) + { + if (!is_callable($callback, false, $callbackKey)) { + throw new Exception\InvalidArgumentException('Invalid callback'); + } + + // functions, methods and classnames are case-insensitive + $callbackKey = strtolower($callbackKey); + + // generate a unique key of object callbacks + if (is_object($callback)) { // Closures & __invoke + $object = $callback; + } elseif (isset($callback[0])) { // array($object, 'method') + $object = $callback[0]; + } + if (isset($object)) { + ErrorHandler::start(); + try { + $serializedObject = serialize($object); + } catch (\Exception $e) { + ErrorHandler::stop(); + throw new Exception\RuntimeException("Can't serialize callback: see previous exception", 0, $e); + } + $error = ErrorHandler::stop(); + + if (!$serializedObject) { + throw new Exception\RuntimeException( + sprintf('Cannot serialize callback%s', ($error ? ': ' . $error->getMessage() : '')), + 0, + $error + ); + } + $callbackKey.= $serializedObject; + } + + return md5($callbackKey) . $this->generateArgumentsKey($args); + } + + /** + * Generate a unique key of the argument part. + * + * @param array $args + * @throws Exception\RuntimeException + * @return string + */ + protected function generateArgumentsKey(array $args) + { + if (!$args) { + return ''; + } + + ErrorHandler::start(); + try { + $serializedArgs = serialize(array_values($args)); + } catch (\Exception $e) { + ErrorHandler::stop(); + throw new Exception\RuntimeException("Can't serialize arguments: see previous exception", 0, $e); + } + $error = ErrorHandler::stop(); + + if (!$serializedArgs) { + throw new Exception\RuntimeException( + sprintf('Cannot serialize arguments%s', ($error ? ': ' . $error->getMessage() : '')), + 0, + $error + ); + } + + return md5($serializedArgs); + } +} diff --git a/library/Zend/Cache/Pattern/CaptureCache.php b/library/Zend/Cache/Pattern/CaptureCache.php new file mode 100755 index 0000000000..7d5cec6981 --- /dev/null +++ b/library/Zend/Cache/Pattern/CaptureCache.php @@ -0,0 +1,388 @@ +detectPageId(); + } + + $that = $this; + ob_start(function ($content) use ($that, $pageId) { + $that->set($content, $pageId); + + // http://php.net/manual/function.ob-start.php + // -> If output_callback returns FALSE original input is sent to the browser. + return false; + }); + + ob_implicit_flush(false); + } + + /** + * Write content to page identity + * + * @param string $content + * @param null|string $pageId + * @throws Exception\LogicException + */ + public function set($content, $pageId = null) + { + $publicDir = $this->getOptions()->getPublicDir(); + if ($publicDir === null) { + throw new Exception\LogicException("Option 'public_dir' no set"); + } + + if ($pageId === null) { + $pageId = $this->detectPageId(); + } + + $path = $this->pageId2Path($pageId); + $file = $path . DIRECTORY_SEPARATOR . $this->pageId2Filename($pageId); + + $this->createDirectoryStructure($publicDir . DIRECTORY_SEPARATOR . $path); + $this->putFileContent($publicDir . DIRECTORY_SEPARATOR . $file, $content); + } + + /** + * Get from cache + * + * @param null|string $pageId + * @return string|null + * @throws Exception\LogicException + * @throws Exception\RuntimeException + */ + public function get($pageId = null) + { + $publicDir = $this->getOptions()->getPublicDir(); + if ($publicDir === null) { + throw new Exception\LogicException("Option 'public_dir' no set"); + } + + if ($pageId === null) { + $pageId = $this->detectPageId(); + } + + $file = $publicDir + . DIRECTORY_SEPARATOR . $this->pageId2Path($pageId) + . DIRECTORY_SEPARATOR . $this->pageId2Filename($pageId); + + if (file_exists($file)) { + ErrorHandler::start(); + $content = file_get_contents($file); + $error = ErrorHandler::stop(); + if ($content === false) { + throw new Exception\RuntimeException("Failed to read cached pageId '{$pageId}'", 0, $error); + } + return $content; + } + } + + /** + * Checks if a cache with given id exists + * + * @param null|string $pageId + * @throws Exception\LogicException + * @return bool + */ + public function has($pageId = null) + { + $publicDir = $this->getOptions()->getPublicDir(); + if ($publicDir === null) { + throw new Exception\LogicException("Option 'public_dir' no set"); + } + + if ($pageId === null) { + $pageId = $this->detectPageId(); + } + + $file = $publicDir + . DIRECTORY_SEPARATOR . $this->pageId2Path($pageId) + . DIRECTORY_SEPARATOR . $this->pageId2Filename($pageId); + + return file_exists($file); + } + + /** + * Remove from cache + * + * @param null|string $pageId + * @throws Exception\LogicException + * @throws Exception\RuntimeException + * @return bool + */ + public function remove($pageId = null) + { + $publicDir = $this->getOptions()->getPublicDir(); + if ($publicDir === null) { + throw new Exception\LogicException("Option 'public_dir' no set"); + } + + if ($pageId === null) { + $pageId = $this->detectPageId(); + } + + $file = $publicDir + . DIRECTORY_SEPARATOR . $this->pageId2Path($pageId) + . DIRECTORY_SEPARATOR . $this->pageId2Filename($pageId); + + if (file_exists($file)) { + ErrorHandler::start(); + $res = unlink($file); + $err = ErrorHandler::stop(); + if (!$res) { + throw new Exception\RuntimeException("Failed to remove cached pageId '{$pageId}'", 0, $err); + } + return true; + } + + return false; + } + + /** + * Clear cached pages matching glob pattern + * + * @param string $pattern + * @throws Exception\LogicException + */ + public function clearByGlob($pattern = '**') + { + $publicDir = $this->getOptions()->getPublicDir(); + if ($publicDir === null) { + throw new Exception\LogicException("Option 'public_dir' no set"); + } + + $it = new \GlobIterator( + $publicDir . '/' . $pattern, + \GlobIterator::CURRENT_AS_SELF | \GlobIterator::SKIP_DOTS | \GlobIterator::UNIX_PATHS + ); + foreach ($it as $pathname => $entry) { + if ($entry->isFile()) { + unlink($pathname); + } + } + } + + /** + * Determine the page to save from the request + * + * @throws Exception\RuntimeException + * @return string + */ + protected function detectPageId() + { + if (!isset($_SERVER['REQUEST_URI'])) { + throw new Exception\RuntimeException("Can't auto-detect current page identity"); + } + + return $_SERVER['REQUEST_URI']; + } + + /** + * Get filename for page id + * + * @param string $pageId + * @return string + */ + protected function pageId2Filename($pageId) + { + if (substr($pageId, -1) === '/') { + return $this->getOptions()->getIndexFilename(); + } + + return basename($pageId); + } + + /** + * Get path for page id + * + * @param string $pageId + * @return string + */ + protected function pageId2Path($pageId) + { + if (substr($pageId, -1) == '/') { + $path = rtrim($pageId, '/'); + } else { + $path = dirname($pageId); + } + + // convert requested "/" to the valid local directory separator + if ('/' != DIRECTORY_SEPARATOR) { + $path = str_replace('/', DIRECTORY_SEPARATOR, $path); + } + + return $path; + } + + /** + * Write content to a file + * + * @param string $file File complete path + * @param string $data Data to write + * @return void + * @throws Exception\RuntimeException + */ + protected function putFileContent($file, $data) + { + $options = $this->getOptions(); + $locking = $options->getFileLocking(); + $perm = $options->getFilePermission(); + $umask = $options->getUmask(); + if ($umask !== false && $perm !== false) { + $perm = $perm & ~$umask; + } + + ErrorHandler::start(); + + $umask = ($umask !== false) ? umask($umask) : false; + $rs = file_put_contents($file, $data, $locking ? LOCK_EX : 0); + if ($umask) { + umask($umask); + } + + if ($rs === false) { + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException("Error writing file '{$file}'", 0, $err); + } + + if ($perm !== false && !chmod($file, $perm)) { + $oct = decoct($perm); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException("chmod('{$file}', 0{$oct}) failed", 0, $err); + } + + ErrorHandler::stop(); + } + + /** + * Creates directory if not already done. + * + * @param string $pathname + * @return void + * @throws Exception\RuntimeException + */ + protected function createDirectoryStructure($pathname) + { + // Directory structure already exists + if (file_exists($pathname)) { + return; + } + + $options = $this->getOptions(); + $perm = $options->getDirPermission(); + $umask = $options->getUmask(); + if ($umask !== false && $perm !== false) { + $perm = $perm & ~$umask; + } + + ErrorHandler::start(); + + if ($perm === false) { + // build-in mkdir function is enough + + $umask = ($umask !== false) ? umask($umask) : false; + $res = mkdir($pathname, ($perm !== false) ? $perm : 0777, true); + + if ($umask !== false) { + umask($umask); + } + + if (!$res) { + $oct = ($perm === false) ? '777' : decoct($perm); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException("mkdir('{$pathname}', 0{$oct}, true) failed", 0, $err); + } + + if ($perm !== false && !chmod($pathname, $perm)) { + $oct = decoct($perm); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException("chmod('{$pathname}', 0{$oct}) failed", 0, $err); + } + } else { + // build-in mkdir function sets permission together with current umask + // which doesn't work well on multo threaded webservers + // -> create directories one by one and set permissions + + // find existing path and missing path parts + $parts = array(); + $path = $pathname; + while (!file_exists($path)) { + array_unshift($parts, basename($path)); + $nextPath = dirname($path); + if ($nextPath === $path) { + break; + } + $path = $nextPath; + } + + // make all missing path parts + foreach ($parts as $part) { + $path.= DIRECTORY_SEPARATOR . $part; + + // create a single directory, set and reset umask immediately + $umask = ($umask !== false) ? umask($umask) : false; + $res = mkdir($path, ($perm === false) ? 0777 : $perm, false); + if ($umask !== false) { + umask($umask); + } + + if (!$res) { + $oct = ($perm === false) ? '777' : decoct($perm); + ErrorHandler::stop(); + throw new Exception\RuntimeException( + "mkdir('{$path}', 0{$oct}, false) failed" + ); + } + + if ($perm !== false && !chmod($path, $perm)) { + $oct = decoct($perm); + ErrorHandler::stop(); + throw new Exception\RuntimeException( + "chmod('{$path}', 0{$oct}) failed" + ); + } + } + } + + ErrorHandler::stop(); + } + + /** + * Returns the generated file name. + * + * @param null|string $pageId + * @return string + */ + public function getFilename($pageId = null) + { + if ($pageId === null) { + $pageId = $this->detectPageId(); + } + + $publicDir = $this->getOptions()->getPublicDir(); + $path = $this->pageId2Path($pageId); + $file = $path . DIRECTORY_SEPARATOR . $this->pageId2Filename($pageId); + + return $publicDir . $file; + } +} diff --git a/library/Zend/Cache/Pattern/ClassCache.php b/library/Zend/Cache/Pattern/ClassCache.php new file mode 100755 index 0000000000..238feed5ef --- /dev/null +++ b/library/Zend/Cache/Pattern/ClassCache.php @@ -0,0 +1,167 @@ +getClass()) { + throw new Exception\InvalidArgumentException("Missing option 'class'"); + } elseif (!$options->getStorage()) { + throw new Exception\InvalidArgumentException("Missing option 'storage'"); + } + return $this; + } + + /** + * Call and cache a class method + * + * @param string $method Method name to call + * @param array $args Method arguments + * @return mixed + * @throws Exception\RuntimeException + * @throws \Exception + */ + public function call($method, array $args = array()) + { + $options = $this->getOptions(); + $classname = $options->getClass(); + $method = strtolower($method); + $callback = $classname . '::' . $method; + + $cache = $options->getCacheByDefault(); + if ($cache) { + $cache = !in_array($method, $options->getClassNonCacheMethods()); + } else { + $cache = in_array($method, $options->getClassCacheMethods()); + } + + if (!$cache) { + if ($args) { + return call_user_func_array($callback, $args); + } else { + return $classname::$method(); + } + } + + return parent::call($callback, $args); + } + + /** + * Generate a unique key in base of a key representing the callback part + * and a key representing the arguments part. + * + * @param string $method The method + * @param array $args Callback arguments + * @return string + * @throws Exception\RuntimeException + */ + public function generateKey($method, array $args = array()) + { + return $this->generateCallbackKey( + $this->getOptions()->getClass() . '::' . $method, + $args + ); + } + + /** + * Generate a unique key in base of a key representing the callback part + * and a key representing the arguments part. + * + * @param callable $callback A valid callback + * @param array $args Callback arguments + * @return string + * @throws Exception\RuntimeException + */ + protected function generateCallbackKey($callback, array $args) + { + $callbackKey = md5(strtolower($callback)); + $argumentKey = $this->generateArgumentsKey($args); + return $callbackKey . $argumentKey; + } + + /** + * Calling a method of the entity. + * + * @param string $method Method name to call + * @param array $args Method arguments + * @return mixed + * @throws Exception\RuntimeException + * @throws \Exception + */ + public function __call($method, array $args) + { + return $this->call($method, $args); + } + + /** + * Set a static property + * + * @param string $name + * @param mixed $value + * @return void + * @see http://php.net/manual/language.oop5.overloading.php#language.oop5.overloading.members + */ + public function __set($name, $value) + { + $class = $this->getOptions()->getClass(); + $class::$name = $value; + } + + /** + * Get a static property + * + * @param string $name + * @return mixed + * @see http://php.net/manual/language.oop5.overloading.php#language.oop5.overloading.members + */ + public function __get($name) + { + $class = $this->getOptions()->getClass(); + return $class::$name; + } + + /** + * Is a static property exists. + * + * @param string $name + * @return bool + */ + public function __isset($name) + { + $class = $this->getOptions()->getClass(); + return isset($class::$name); + } + + /** + * Unset a static property + * + * @param string $name + * @return void + */ + public function __unset($name) + { + $class = $this->getOptions()->getClass(); + unset($class::$name); + } +} diff --git a/library/Zend/Cache/Pattern/ObjectCache.php b/library/Zend/Cache/Pattern/ObjectCache.php new file mode 100755 index 0000000000..9ef2a24142 --- /dev/null +++ b/library/Zend/Cache/Pattern/ObjectCache.php @@ -0,0 +1,284 @@ +getObject()) { + throw new Exception\InvalidArgumentException("Missing option 'object'"); + } elseif (!$options->getStorage()) { + throw new Exception\InvalidArgumentException("Missing option 'storage'"); + } + } + + /** + * Call and cache a class method + * + * @param string $method Method name to call + * @param array $args Method arguments + * @return mixed + * @throws Exception\RuntimeException + * @throws \Exception + */ + public function call($method, array $args = array()) + { + $options = $this->getOptions(); + $object = $options->getObject(); + $method = strtolower($method); + + // handle magic methods + switch ($method) { + case '__set': + $property = array_shift($args); + $value = array_shift($args); + + $object->{$property} = $value; + + if (!$options->getObjectCacheMagicProperties() + || property_exists($object, $property) + ) { + // no caching if property isn't magic + // or caching magic properties is disabled + return; + } + + // remove cached __get and __isset + $removeKeys = null; + if (method_exists($object, '__get')) { + $removeKeys[] = $this->generateKey('__get', array($property)); + } + if (method_exists($object, '__isset')) { + $removeKeys[] = $this->generateKey('__isset', array($property)); + } + if ($removeKeys) { + $options->getStorage()->removeItems($removeKeys); + } + return; + + case '__get': + $property = array_shift($args); + + if (!$options->getObjectCacheMagicProperties() + || property_exists($object, $property) + ) { + // no caching if property isn't magic + // or caching magic properties is disabled + return $object->{$property}; + } + + array_unshift($args, $property); + return parent::call(array($object, '__get'), $args); + + case '__isset': + $property = array_shift($args); + + if (!$options->getObjectCacheMagicProperties() + || property_exists($object, $property) + ) { + // no caching if property isn't magic + // or caching magic properties is disabled + return isset($object->{$property}); + } + + return parent::call(array($object, '__isset'), array($property)); + + case '__unset': + $property = array_shift($args); + + unset($object->{$property}); + + if (!$options->getObjectCacheMagicProperties() + || property_exists($object, $property) + ) { + // no caching if property isn't magic + // or caching magic properties is disabled + return; + } + + // remove previous cached __get and __isset calls + $removeKeys = null; + if (method_exists($object, '__get')) { + $removeKeys[] = $this->generateKey('__get', array($property)); + } + if (method_exists($object, '__isset')) { + $removeKeys[] = $this->generateKey('__isset', array($property)); + } + if ($removeKeys) { + $options->getStorage()->removeItems($removeKeys); + } + return; + } + + $cache = $options->getCacheByDefault(); + if ($cache) { + $cache = !in_array($method, $options->getObjectNonCacheMethods()); + } else { + $cache = in_array($method, $options->getObjectCacheMethods()); + } + + if (!$cache) { + if ($args) { + return call_user_func_array(array($object, $method), $args); + } + return $object->{$method}(); + } + + return parent::call(array($object, $method), $args); + } + + /** + * Generate a unique key in base of a key representing the callback part + * and a key representing the arguments part. + * + * @param string $method The method + * @param array $args Callback arguments + * @return string + * @throws Exception\RuntimeException + */ + public function generateKey($method, array $args = array()) + { + return $this->generateCallbackKey( + array($this->getOptions()->getObject(), $method), + $args + ); + } + + /** + * Generate a unique key in base of a key representing the callback part + * and a key representing the arguments part. + * + * @param callable $callback A valid callback + * @param array $args Callback arguments + * @return string + * @throws Exception\RuntimeException + */ + protected function generateCallbackKey($callback, array $args = array()) + { + $callbackKey = md5($this->getOptions()->getObjectKey() . '::' . strtolower($callback[1])); + $argumentKey = $this->generateArgumentsKey($args); + return $callbackKey . $argumentKey; + } + + /** + * Class method call handler + * + * @param string $method Method name to call + * @param array $args Method arguments + * @return mixed + * @throws Exception\RuntimeException + * @throws \Exception + */ + public function __call($method, array $args) + { + return $this->call($method, $args); + } + + /** + * Writing data to properties. + * + * NOTE: + * Magic properties will be cached too if the option cacheMagicProperties + * is enabled and the property doesn't exist in real. If so it calls __set + * and removes cached data of previous __get and __isset calls. + * + * @param string $name + * @param mixed $value + * @return void + * @see http://php.net/manual/language.oop5.overloading.php#language.oop5.overloading.members + */ + public function __set($name, $value) + { + return $this->call('__set', array($name, $value)); + } + + /** + * Reading data from properties. + * + * NOTE: + * Magic properties will be cached too if the option cacheMagicProperties + * is enabled and the property doesn't exist in real. If so it calls __get. + * + * @param string $name + * @return mixed + * @see http://php.net/manual/language.oop5.overloading.php#language.oop5.overloading.members + */ + public function __get($name) + { + return $this->call('__get', array($name)); + } + + /** + * Checking existing properties. + * + * NOTE: + * Magic properties will be cached too if the option cacheMagicProperties + * is enabled and the property doesn't exist in real. If so it calls __get. + * + * @param string $name + * @return bool + * @see http://php.net/manual/language.oop5.overloading.php#language.oop5.overloading.members + */ + public function __isset($name) + { + return $this->call('__isset', array($name)); + } + + /** + * Unseting a property. + * + * NOTE: + * Magic properties will be cached too if the option cacheMagicProperties + * is enabled and the property doesn't exist in real. If so it removes + * previous cached __isset and __get calls. + * + * @param string $name + * @return void + * @see http://php.net/manual/language.oop5.overloading.php#language.oop5.overloading.members + */ + public function __unset($name) + { + return $this->call('__unset', array($name)); + } + + /** + * Handle casting to string + * + * @return string + * @see http://php.net/manual/language.oop5.magic.php#language.oop5.magic.tostring + */ + public function __toString() + { + return $this->call('__toString'); + } + + /** + * Handle invoke calls + * + * @return mixed + * @see http://php.net/manual/language.oop5.magic.php#language.oop5.magic.invoke + */ + public function __invoke() + { + return $this->call('__invoke', func_get_args()); + } +} diff --git a/library/Zend/Cache/Pattern/OutputCache.php b/library/Zend/Cache/Pattern/OutputCache.php new file mode 100755 index 0000000000..549e17122b --- /dev/null +++ b/library/Zend/Cache/Pattern/OutputCache.php @@ -0,0 +1,89 @@ +getStorage()) { + throw new Exception\InvalidArgumentException("Missing option 'storage'"); + } + + return $this; + } + + /** + * if there is a cached item with the given key display it's data and return true + * else start buffering output until end() is called or the script ends. + * + * @param string $key Key + * @throws Exception\MissingKeyException if key is missing + * @return bool + */ + public function start($key) + { + if (($key = (string) $key) === '') { + throw new Exception\MissingKeyException('Missing key to read/write output from cache'); + } + + $success = null; + $data = $this->getOptions()->getStorage()->getItem($key, $success); + if ($success) { + echo $data; + return true; + } + + ob_start(); + ob_implicit_flush(false); + $this->keyStack[] = $key; + return false; + } + + /** + * Stops buffering output, write buffered data to cache using the given key on start() + * and displays the buffer. + * + * @throws Exception\RuntimeException if output cache not started or buffering not active + * @return bool TRUE on success, FALSE on failure writing to cache + */ + public function end() + { + $key = array_pop($this->keyStack); + if ($key === null) { + throw new Exception\RuntimeException('Output cache not started'); + } + + $output = ob_get_flush(); + if ($output === false) { + throw new Exception\RuntimeException('Output buffering not active'); + } + + return $this->getOptions()->getStorage()->setItem($key, $output); + } +} diff --git a/library/Zend/Cache/Pattern/PatternInterface.php b/library/Zend/Cache/Pattern/PatternInterface.php new file mode 100755 index 0000000000..a79bf51883 --- /dev/null +++ b/library/Zend/Cache/Pattern/PatternInterface.php @@ -0,0 +1,28 @@ +filePermission = false; + $this->dirPermission = false; + } + + parent::__construct($options); + } + + /** + * Set flag indicating whether or not to cache by default + * + * Used by: + * - ClassCache + * - ObjectCache + * + * @param bool $cacheByDefault + * @return PatternOptions + */ + public function setCacheByDefault($cacheByDefault) + { + $this->cacheByDefault = $cacheByDefault; + return $this; + } + + /** + * Do we cache by default? + * + * Used by: + * - ClassCache + * - ObjectCache + * + * @return bool + */ + public function getCacheByDefault() + { + return $this->cacheByDefault; + } + + /** + * Set whether or not to cache output + * + * Used by: + * - CallbackCache + * - ClassCache + * - ObjectCache + * + * @param bool $cacheOutput + * @return PatternOptions + */ + public function setCacheOutput($cacheOutput) + { + $this->cacheOutput = (bool) $cacheOutput; + return $this; + } + + /** + * Will we cache output? + * + * Used by: + * - CallbackCache + * - ClassCache + * - ObjectCache + * + * @return bool + */ + public function getCacheOutput() + { + return $this->cacheOutput; + } + + /** + * Set class name + * + * Used by: + * - ClassCache + * + * @param string $class + * @throws Exception\InvalidArgumentException + * @return PatternOptions + */ + public function setClass($class) + { + if (!is_string($class)) { + throw new Exception\InvalidArgumentException('Invalid classname provided; must be a string'); + } + $this->class = $class; + return $this; + } + + /** + * Get class name + * + * Used by: + * - ClassCache + * + * @return null|string + */ + public function getClass() + { + return $this->class; + } + + /** + * Set list of method return values to cache + * + * Used by: + * - ClassCache + * + * @param array $classCacheMethods + * @return PatternOptions + */ + public function setClassCacheMethods(array $classCacheMethods) + { + $this->classCacheMethods = $this->recursiveStrtolower($classCacheMethods); + return $this; + } + + /** + * Get list of methods from which to cache return values + * + * Used by: + * - ClassCache + * + * @return array + */ + public function getClassCacheMethods() + { + return $this->classCacheMethods; + } + + /** + * Set list of method return values NOT to cache + * + * Used by: + * - ClassCache + * + * @param array $classNonCacheMethods + * @return PatternOptions + */ + public function setClassNonCacheMethods(array $classNonCacheMethods) + { + $this->classNonCacheMethods = $this->recursiveStrtolower($classNonCacheMethods); + return $this; + } + + /** + * Get list of methods from which NOT to cache return values + * + * Used by: + * - ClassCache + * + * @return array + */ + public function getClassNonCacheMethods() + { + return $this->classNonCacheMethods; + } + + /** + * Set directory permission + * + * @param false|int $dirPermission + * @throws Exception\InvalidArgumentException + * @return PatternOptions + */ + public function setDirPermission($dirPermission) + { + if ($dirPermission !== false) { + if (is_string($dirPermission)) { + $dirPermission = octdec($dirPermission); + } else { + $dirPermission = (int) $dirPermission; + } + + // validate + if (($dirPermission & 0700) != 0700) { + throw new Exception\InvalidArgumentException( + 'Invalid directory permission: need permission to execute, read and write by owner' + ); + } + } + + $this->dirPermission = $dirPermission; + return $this; + } + + /** + * Gets directory permission + * + * @return false|int + */ + public function getDirPermission() + { + return $this->dirPermission; + } + + /** + * Set umask + * + * Used by: + * - CaptureCache + * + * @param false|int $umask + * @throws Exception\InvalidArgumentException + * @return PatternOptions + */ + public function setUmask($umask) + { + if ($umask !== false) { + if (is_string($umask)) { + $umask = octdec($umask); + } else { + $umask = (int) $umask; + } + + // validate + if ($umask & 0700) { + throw new Exception\InvalidArgumentException( + 'Invalid umask: need permission to execute, read and write by owner' + ); + } + + // normalize + $umask = $umask & 0777; + } + + $this->umask = $umask; + return $this; + } + + /** + * Get umask + * + * Used by: + * - CaptureCache + * + * @return false|int + */ + public function getUmask() + { + return $this->umask; + } + + /** + * Set whether or not file locking should be used + * + * Used by: + * - CaptureCache + * + * @param bool $fileLocking + * @return PatternOptions + */ + public function setFileLocking($fileLocking) + { + $this->fileLocking = (bool) $fileLocking; + return $this; + } + + /** + * Is file locking enabled? + * + * Used by: + * - CaptureCache + * + * @return bool + */ + public function getFileLocking() + { + return $this->fileLocking; + } + + /** + * Set file permission + * + * @param false|int $filePermission + * @throws Exception\InvalidArgumentException + * @return PatternOptions + */ + public function setFilePermission($filePermission) + { + if ($filePermission !== false) { + if (is_string($filePermission)) { + $filePermission = octdec($filePermission); + } else { + $filePermission = (int) $filePermission; + } + + // validate + if (($filePermission & 0600) != 0600) { + throw new Exception\InvalidArgumentException( + 'Invalid file permission: need permission to read and write by owner' + ); + } elseif ($filePermission & 0111) { + throw new Exception\InvalidArgumentException( + "Invalid file permission: Files shoudn't be executable" + ); + } + } + + $this->filePermission = $filePermission; + return $this; + } + + /** + * Gets file permission + * + * @return false|int + */ + public function getFilePermission() + { + return $this->filePermission; + } + + /** + * Set value for index filename + * + * @param string $indexFilename + * @return PatternOptions + */ + public function setIndexFilename($indexFilename) + { + $this->indexFilename = (string) $indexFilename; + return $this; + } + + /** + * Get value for index filename + * + * @return string + */ + public function getIndexFilename() + { + return $this->indexFilename; + } + + /** + * Set object to cache + * + * @param mixed $object + * @throws Exception\InvalidArgumentException + * @return PatternOptions + */ + public function setObject($object) + { + if (!is_object($object)) { + throw new Exception\InvalidArgumentException( + sprintf('%s expects an object; received "%s"', __METHOD__, gettype($object)) + ); + } + $this->object = $object; + return $this; + } + + /** + * Get object to cache + * + * @return null|object + */ + public function getObject() + { + return $this->object; + } + + /** + * Set flag indicating whether or not to cache magic properties + * + * Used by: + * - ObjectCache + * + * @param bool $objectCacheMagicProperties + * @return PatternOptions + */ + public function setObjectCacheMagicProperties($objectCacheMagicProperties) + { + $this->objectCacheMagicProperties = (bool) $objectCacheMagicProperties; + return $this; + } + + /** + * Should we cache magic properties? + * + * Used by: + * - ObjectCache + * + * @return bool + */ + public function getObjectCacheMagicProperties() + { + return $this->objectCacheMagicProperties; + } + + /** + * Set list of object methods for which to cache return values + * + * @param array $objectCacheMethods + * @return PatternOptions + * @throws Exception\InvalidArgumentException + */ + public function setObjectCacheMethods(array $objectCacheMethods) + { + $this->objectCacheMethods = $this->normalizeObjectMethods($objectCacheMethods); + return $this; + } + + /** + * Get list of object methods for which to cache return values + * + * @return array + */ + public function getObjectCacheMethods() + { + return $this->objectCacheMethods; + } + + /** + * Set the object key part. + * + * Used to generate a callback key in order to speed up key generation. + * + * Used by: + * - ObjectCache + * + * @param mixed $objectKey + * @return PatternOptions + */ + public function setObjectKey($objectKey) + { + if ($objectKey !== null) { + $this->objectKey = (string) $objectKey; + } else { + $this->objectKey = null; + } + return $this; + } + + /** + * Get object key + * + * Used by: + * - ObjectCache + * + * @return string + */ + public function getObjectKey() + { + if (!$this->objectKey) { + return get_class($this->getObject()); + } + return $this->objectKey; + } + + /** + * Set list of object methods for which NOT to cache return values + * + * @param array $objectNonCacheMethods + * @return PatternOptions + * @throws Exception\InvalidArgumentException + */ + public function setObjectNonCacheMethods(array $objectNonCacheMethods) + { + $this->objectNonCacheMethods = $this->normalizeObjectMethods($objectNonCacheMethods); + return $this; + } + + /** + * Get list of object methods for which NOT to cache return values + * + * @return array + */ + public function getObjectNonCacheMethods() + { + return $this->objectNonCacheMethods; + } + + /** + * Set location of public directory + * + * Used by: + * - CaptureCache + * + * @param string $publicDir + * @throws Exception\InvalidArgumentException + * @return PatternOptions + */ + public function setPublicDir($publicDir) + { + $publicDir = (string) $publicDir; + + if (!is_dir($publicDir)) { + throw new Exception\InvalidArgumentException( + "Public directory '{$publicDir}' not found or not a directory" + ); + } elseif (!is_writable($publicDir)) { + throw new Exception\InvalidArgumentException( + "Public directory '{$publicDir}' not writable" + ); + } elseif (!is_readable($publicDir)) { + throw new Exception\InvalidArgumentException( + "Public directory '{$publicDir}' not readable" + ); + } + + $this->publicDir = rtrim(realpath($publicDir), DIRECTORY_SEPARATOR); + return $this; + } + + /** + * Get location of public directory + * + * Used by: + * - CaptureCache + * + * @return null|string + */ + public function getPublicDir() + { + return $this->publicDir; + } + + /** + * Set storage adapter + * + * Required for the following Pattern classes: + * - CallbackCache + * - ClassCache + * - ObjectCache + * - OutputCache + * + * @param string|array|Storage $storage + * @return PatternOptions + */ + public function setStorage($storage) + { + $this->storage = $this->storageFactory($storage); + return $this; + } + + /** + * Get storage adapter + * + * Used by: + * - CallbackCache + * - ClassCache + * - ObjectCache + * - OutputCache + * + * @return null|Storage + */ + public function getStorage() + { + return $this->storage; + } + + /** + * Recursively apply strtolower on all values of an array, and return as a + * list of unique values + * + * @param array $array + * @return array + */ + protected function recursiveStrtolower(array $array) + { + return array_values(array_unique(array_map('strtolower', $array))); + } + + /** + * Normalize object methods + * + * Recursively casts values to lowercase, then determines if any are in a + * list of methods not handled, raising an exception if so. + * + * @param array $methods + * @return array + * @throws Exception\InvalidArgumentException + */ + protected function normalizeObjectMethods(array $methods) + { + $methods = $this->recursiveStrtolower($methods); + $intersect = array_intersect(array('__set', '__get', '__unset', '__isset'), $methods); + if (!empty($intersect)) { + throw new Exception\InvalidArgumentException( + "Magic properties are handled by option 'cache_magic_properties'" + ); + } + return $methods; + } + + /** + * Create a storage object from a given specification + * + * @param array|string|Storage $storage + * @throws Exception\InvalidArgumentException + * @return Storage + */ + protected function storageFactory($storage) + { + if (is_array($storage)) { + $storage = StorageFactory::factory($storage); + } elseif (is_string($storage)) { + $storage = StorageFactory::adapterFactory($storage); + } elseif (!($storage instanceof Storage)) { + throw new Exception\InvalidArgumentException( + 'The storage must be an instanceof Zend\Cache\Storage\StorageInterface ' + . 'or an array passed to Zend\Cache\Storage::factory ' + . 'or simply the name of the storage adapter' + ); + } + + return $storage; + } +} diff --git a/library/Zend/Cache/PatternFactory.php b/library/Zend/Cache/PatternFactory.php new file mode 100755 index 0000000000..dceed0032f --- /dev/null +++ b/library/Zend/Cache/PatternFactory.php @@ -0,0 +1,92 @@ +setOptions($options); + return $patternName; + } + + $pattern = static::getPluginManager()->get($patternName); + $pattern->setOptions($options); + return $pattern; + } + + /** + * Get the pattern plugin manager + * + * @return PatternPluginManager + */ + public static function getPluginManager() + { + if (static::$plugins === null) { + static::$plugins = new PatternPluginManager(); + } + + return static::$plugins; + } + + /** + * Set the pattern plugin manager + * + * @param PatternPluginManager $plugins + * @return void + */ + public static function setPluginManager(PatternPluginManager $plugins) + { + static::$plugins = $plugins; + } + + /** + * Reset pattern plugin manager to default + * + * @return void + */ + public static function resetPluginManager() + { + static::$plugins = null; + } +} diff --git a/library/Zend/Cache/PatternPluginManager.php b/library/Zend/Cache/PatternPluginManager.php new file mode 100755 index 0000000000..7d5d0e1a69 --- /dev/null +++ b/library/Zend/Cache/PatternPluginManager.php @@ -0,0 +1,66 @@ + 'Zend\Cache\Pattern\CallbackCache', + 'capture' => 'Zend\Cache\Pattern\CaptureCache', + 'class' => 'Zend\Cache\Pattern\ClassCache', + 'object' => 'Zend\Cache\Pattern\ObjectCache', + 'output' => 'Zend\Cache\Pattern\OutputCache', + 'page' => 'Zend\Cache\Pattern\PageCache', + ); + + /** + * Don't share by default + * + * @var array + */ + protected $shareByDefault = false; + + /** + * Validate the plugin + * + * Checks that the pattern adapter loaded is an instance of Pattern\PatternInterface. + * + * @param mixed $plugin + * @return void + * @throws Exception\RuntimeException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof Pattern\PatternInterface) { + // we're okay + return; + } + + throw new Exception\RuntimeException(sprintf( + 'Plugin of type %s is invalid; must implement %s\Pattern\PatternInterface', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/library/Zend/Cache/README.md b/library/Zend/Cache/README.md new file mode 100755 index 0000000000..62aa9e0e0c --- /dev/null +++ b/library/Zend/Cache/README.md @@ -0,0 +1,14 @@ +Cache Component from ZF2 +======================== + +This is the Cache component for ZF2. + +- File issues at https://github.com/zendframework/zf2/issues +- Create pull requests against https://github.com/zendframework/zf2 +- Documentation is at http://framework.zend.com/docs + +LICENSE +------- + +The files in this archive are released under the [Zend Framework +license](http://framework.zend.com/license), which is a 3-clause BSD license. diff --git a/library/Zend/Cache/Service/StorageCacheAbstractServiceFactory.php b/library/Zend/Cache/Service/StorageCacheAbstractServiceFactory.php new file mode 100755 index 0000000000..66eb6505e5 --- /dev/null +++ b/library/Zend/Cache/Service/StorageCacheAbstractServiceFactory.php @@ -0,0 +1,88 @@ +getConfig($services); + if (empty($config)) { + return false; + } + + return (isset($config[$requestedName]) && is_array($config[$requestedName])); + } + + /** + * @param ServiceLocatorInterface $services + * @param string $name + * @param string $requestedName + * @return \Zend\Cache\Storage\StorageInterface + */ + public function createServiceWithName(ServiceLocatorInterface $services, $name, $requestedName) + { + $config = $this->getConfig($services); + $config = $config[$requestedName]; + return StorageFactory::factory($config); + } + + /** + * Retrieve cache configuration, if any + * + * @param ServiceLocatorInterface $services + * @return array + */ + protected function getConfig(ServiceLocatorInterface $services) + { + if ($this->config !== null) { + return $this->config; + } + + if (!$services->has('Config')) { + $this->config = array(); + return $this->config; + } + + $config = $services->get('Config'); + if (!isset($config[$this->configKey])) { + $this->config = array(); + return $this->config; + } + + $this->config = $config[$this->configKey]; + return $this->config; + } +} diff --git a/library/Zend/Cache/Service/StorageCacheFactory.php b/library/Zend/Cache/Service/StorageCacheFactory.php new file mode 100755 index 0000000000..f2c2049936 --- /dev/null +++ b/library/Zend/Cache/Service/StorageCacheFactory.php @@ -0,0 +1,30 @@ +get('Config'); + $cacheConfig = isset($config['cache']) ? $config['cache'] : array(); + $cache = StorageFactory::factory($cacheConfig); + + return $cache; + } +} diff --git a/library/Zend/Cache/Storage/Adapter/AbstractAdapter.php b/library/Zend/Cache/Storage/Adapter/AbstractAdapter.php new file mode 100755 index 0000000000..71edf763e8 --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/AbstractAdapter.php @@ -0,0 +1,1580 @@ +setOptions($options); + } + } + + /** + * Destructor + * + * detach all registered plugins to free + * event handles of event manager + * + * @return void + */ + public function __destruct() + { + foreach ($this->getPluginRegistry() as $plugin) { + $this->removePlugin($plugin); + } + + if ($this->eventHandles) { + $events = $this->getEventManager(); + foreach ($this->eventHandles as $handle) { + $events->detach($handle); + } + } + } + + /* configuration */ + + /** + * Set options. + * + * @param array|Traversable|AdapterOptions $options + * @return AbstractAdapter + * @see getOptions() + */ + public function setOptions($options) + { + if ($this->options !== $options) { + if (!$options instanceof AdapterOptions) { + $options = new AdapterOptions($options); + } + + if ($this->options) { + $this->options->setAdapter(null); + } + $options->setAdapter($this); + $this->options = $options; + + $event = new Event('option', $this, new ArrayObject($options->toArray())); + $this->getEventManager()->trigger($event); + } + return $this; + } + + /** + * Get options. + * + * @return AdapterOptions + * @see setOptions() + */ + public function getOptions() + { + if (!$this->options) { + $this->setOptions(new AdapterOptions()); + } + return $this->options; + } + + /** + * Enable/Disable caching. + * + * Alias of setWritable and setReadable. + * + * @see setWritable() + * @see setReadable() + * @param bool $flag + * @return AbstractAdapter + */ + public function setCaching($flag) + { + $flag = (bool) $flag; + $options = $this->getOptions(); + $options->setWritable($flag); + $options->setReadable($flag); + return $this; + } + + /** + * Get caching enabled. + * + * Alias of getWritable and getReadable. + * + * @see getWritable() + * @see getReadable() + * @return bool + */ + public function getCaching() + { + $options = $this->getOptions(); + return ($options->getWritable() && $options->getReadable()); + } + + /* Event/Plugin handling */ + + /** + * Get the event manager + * + * @return EventManagerInterface + */ + public function getEventManager() + { + if ($this->events === null) { + $this->events = new EventManager(array(__CLASS__, get_class($this))); + } + return $this->events; + } + + /** + * Trigger a pre event and return the event response collection + * + * @param string $eventName + * @param ArrayObject $args + * @return \Zend\EventManager\ResponseCollection All handler return values + */ + protected function triggerPre($eventName, ArrayObject $args) + { + return $this->getEventManager()->trigger(new Event($eventName . '.pre', $this, $args)); + } + + /** + * Triggers the PostEvent and return the result value. + * + * @param string $eventName + * @param ArrayObject $args + * @param mixed $result + * @return mixed + */ + protected function triggerPost($eventName, ArrayObject $args, & $result) + { + $postEvent = new PostEvent($eventName . '.post', $this, $args, $result); + $eventRs = $this->getEventManager()->trigger($postEvent); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + return $postEvent->getResult(); + } + + /** + * Trigger an exception event + * + * If the ExceptionEvent has the flag "throwException" enabled throw the + * exception after trigger else return the result. + * + * @param string $eventName + * @param ArrayObject $args + * @param mixed $result + * @param \Exception $exception + * @throws Exception\ExceptionInterface + * @return mixed + */ + protected function triggerException($eventName, ArrayObject $args, & $result, \Exception $exception) + { + $exceptionEvent = new ExceptionEvent($eventName . '.exception', $this, $args, $result, $exception); + $eventRs = $this->getEventManager()->trigger($exceptionEvent); + + if ($exceptionEvent->getThrowException()) { + throw $exceptionEvent->getException(); + } + + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + return $exceptionEvent->getResult(); + } + + /** + * Check if a plugin is registered + * + * @param Plugin\PluginInterface $plugin + * @return bool + */ + public function hasPlugin(Plugin\PluginInterface $plugin) + { + $registry = $this->getPluginRegistry(); + return $registry->contains($plugin); + } + + /** + * Register a plugin + * + * @param Plugin\PluginInterface $plugin + * @param int $priority + * @return AbstractAdapter Fluent interface + * @throws Exception\LogicException + */ + public function addPlugin(Plugin\PluginInterface $plugin, $priority = 1) + { + $registry = $this->getPluginRegistry(); + if ($registry->contains($plugin)) { + throw new Exception\LogicException(sprintf( + 'Plugin of type "%s" already registered', + get_class($plugin) + )); + } + + $plugin->attach($this->getEventManager(), $priority); + $registry->attach($plugin); + + return $this; + } + + /** + * Unregister an already registered plugin + * + * @param Plugin\PluginInterface $plugin + * @return AbstractAdapter Fluent interface + * @throws Exception\LogicException + */ + public function removePlugin(Plugin\PluginInterface $plugin) + { + $registry = $this->getPluginRegistry(); + if ($registry->contains($plugin)) { + $plugin->detach($this->getEventManager()); + $registry->detach($plugin); + } + return $this; + } + + /** + * Return registry of plugins + * + * @return SplObjectStorage + */ + public function getPluginRegistry() + { + if (!$this->pluginRegistry instanceof SplObjectStorage) { + $this->pluginRegistry = new SplObjectStorage(); + } + return $this->pluginRegistry; + } + + /* reading */ + + /** + * Get an item. + * + * @param string $key + * @param bool $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + * + * @triggers getItem.pre(PreEvent) + * @triggers getItem.post(PostEvent) + * @triggers getItem.exception(ExceptionEvent) + */ + public function getItem($key, & $success = null, & $casToken = null) + { + if (!$this->getOptions()->getReadable()) { + $success = false; + return null; + } + + $this->normalizeKey($key); + + $argn = func_num_args(); + $args = array( + 'key' => & $key, + ); + if ($argn > 1) { + $args['success'] = & $success; + } + if ($argn > 2) { + $args['casToken'] = & $casToken; + } + $args = new ArrayObject($args); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + if ($args->offsetExists('success') && $args->offsetExists('casToken')) { + $result = $this->internalGetItem($args['key'], $args['success'], $args['casToken']); + } elseif ($args->offsetExists('success')) { + $result = $this->internalGetItem($args['key'], $args['success']); + } else { + $result = $this->internalGetItem($args['key']); + } + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to get an item. + * + * @param string $normalizedKey + * @param bool $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + */ + abstract protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null); + + /** + * Get multiple items. + * + * @param array $keys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + * + * @triggers getItems.pre(PreEvent) + * @triggers getItems.post(PostEvent) + * @triggers getItems.exception(ExceptionEvent) + */ + public function getItems(array $keys) + { + if (!$this->getOptions()->getReadable()) { + return array(); + } + + $this->normalizeKeys($keys); + $args = new ArrayObject(array( + 'keys' => & $keys, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalGetItems($args['keys']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = array(); + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to get multiple items. + * + * @param array $normalizedKeys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + */ + protected function internalGetItems(array & $normalizedKeys) + { + $success = null; + $result = array(); + foreach ($normalizedKeys as $normalizedKey) { + $value = $this->internalGetItem($normalizedKey, $success); + if ($success) { + $result[$normalizedKey] = $value; + } + } + + return $result; + } + + /** + * Test if an item exists. + * + * @param string $key + * @return bool + * @throws Exception\ExceptionInterface + * + * @triggers hasItem.pre(PreEvent) + * @triggers hasItem.post(PostEvent) + * @triggers hasItem.exception(ExceptionEvent) + */ + public function hasItem($key) + { + if (!$this->getOptions()->getReadable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'key' => & $key, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalHasItem($args['key']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalHasItem(& $normalizedKey) + { + $success = null; + $this->internalGetItem($normalizedKey, $success); + return $success; + } + + /** + * Test multiple items. + * + * @param array $keys + * @return array Array of found keys + * @throws Exception\ExceptionInterface + * + * @triggers hasItems.pre(PreEvent) + * @triggers hasItems.post(PostEvent) + * @triggers hasItems.exception(ExceptionEvent) + */ + public function hasItems(array $keys) + { + if (!$this->getOptions()->getReadable()) { + return array(); + } + + $this->normalizeKeys($keys); + $args = new ArrayObject(array( + 'keys' => & $keys, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalHasItems($args['keys']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = array(); + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to test multiple items. + * + * @param array $normalizedKeys + * @return array Array of found keys + * @throws Exception\ExceptionInterface + */ + protected function internalHasItems(array & $normalizedKeys) + { + $result = array(); + foreach ($normalizedKeys as $normalizedKey) { + if ($this->internalHasItem($normalizedKey)) { + $result[] = $normalizedKey; + } + } + return $result; + } + + /** + * Get metadata of an item. + * + * @param string $key + * @return array|bool Metadata on success, false on failure + * @throws Exception\ExceptionInterface + * + * @triggers getMetadata.pre(PreEvent) + * @triggers getMetadata.post(PostEvent) + * @triggers getMetadata.exception(ExceptionEvent) + */ + public function getMetadata($key) + { + if (!$this->getOptions()->getReadable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'key' => & $key, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalGetMetadata($args['key']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to get metadata of an item. + * + * @param string $normalizedKey + * @return array|bool Metadata on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetMetadata(& $normalizedKey) + { + if (!$this->internalHasItem($normalizedKey)) { + return false; + } + + return array(); + } + + /** + * Get multiple metadata + * + * @param array $keys + * @return array Associative array of keys and metadata + * @throws Exception\ExceptionInterface + * + * @triggers getMetadatas.pre(PreEvent) + * @triggers getMetadatas.post(PostEvent) + * @triggers getMetadatas.exception(ExceptionEvent) + */ + public function getMetadatas(array $keys) + { + if (!$this->getOptions()->getReadable()) { + return array(); + } + + $this->normalizeKeys($keys); + $args = new ArrayObject(array( + 'keys' => & $keys, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalGetMetadatas($args['keys']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = array(); + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to get multiple metadata + * + * @param array $normalizedKeys + * @return array Associative array of keys and metadata + * @throws Exception\ExceptionInterface + */ + protected function internalGetMetadatas(array & $normalizedKeys) + { + $result = array(); + foreach ($normalizedKeys as $normalizedKey) { + $metadata = $this->internalGetMetadata($normalizedKey); + if ($metadata !== false) { + $result[$normalizedKey] = $metadata; + } + } + return $result; + } + + /* writing */ + + /** + * Store an item. + * + * @param string $key + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + * + * @triggers setItem.pre(PreEvent) + * @triggers setItem.post(PostEvent) + * @triggers setItem.exception(ExceptionEvent) + */ + public function setItem($key, $value) + { + if (!$this->getOptions()->getWritable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'key' => & $key, + 'value' => & $value, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalSetItem($args['key'], $args['value']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to store an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + abstract protected function internalSetItem(& $normalizedKey, & $value); + + /** + * Store multiple items. + * + * @param array $keyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + * + * @triggers setItems.pre(PreEvent) + * @triggers setItems.post(PostEvent) + * @triggers setItems.exception(ExceptionEvent) + */ + public function setItems(array $keyValuePairs) + { + if (!$this->getOptions()->getWritable()) { + return array_keys($keyValuePairs); + } + + $this->normalizeKeyValuePairs($keyValuePairs); + $args = new ArrayObject(array( + 'keyValuePairs' => & $keyValuePairs, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalSetItems($args['keyValuePairs']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = array_keys($keyValuePairs); + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to store multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalSetItems(array & $normalizedKeyValuePairs) + { + $failedKeys = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + if (!$this->internalSetItem($normalizedKey, $value)) { + $failedKeys[] = $normalizedKey; + } + } + return $failedKeys; + } + + /** + * Add an item. + * + * @param string $key + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + * + * @triggers addItem.pre(PreEvent) + * @triggers addItem.post(PostEvent) + * @triggers addItem.exception(ExceptionEvent) + */ + public function addItem($key, $value) + { + if (!$this->getOptions()->getWritable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'key' => & $key, + 'value' => & $value, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalAddItem($args['key'], $args['value']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to add an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalAddItem(& $normalizedKey, & $value) + { + if ($this->internalHasItem($normalizedKey)) { + return false; + } + return $this->internalSetItem($normalizedKey, $value); + } + + /** + * Add multiple items. + * + * @param array $keyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + * + * @triggers addItems.pre(PreEvent) + * @triggers addItems.post(PostEvent) + * @triggers addItems.exception(ExceptionEvent) + */ + public function addItems(array $keyValuePairs) + { + if (!$this->getOptions()->getWritable()) { + return array_keys($keyValuePairs); + } + + $this->normalizeKeyValuePairs($keyValuePairs); + $args = new ArrayObject(array( + 'keyValuePairs' => & $keyValuePairs, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalAddItems($args['keyValuePairs']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = array_keys($keyValuePairs); + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to add multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalAddItems(array & $normalizedKeyValuePairs) + { + $result = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + if (!$this->internalAddItem($normalizedKey, $value)) { + $result[] = $normalizedKey; + } + } + return $result; + } + + /** + * Replace an existing item. + * + * @param string $key + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + * + * @triggers replaceItem.pre(PreEvent) + * @triggers replaceItem.post(PostEvent) + * @triggers replaceItem.exception(ExceptionEvent) + */ + public function replaceItem($key, $value) + { + if (!$this->getOptions()->getWritable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'key' => & $key, + 'value' => & $value, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalReplaceItem($args['key'], $args['value']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to replace an existing item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalReplaceItem(& $normalizedKey, & $value) + { + if (!$this->internalhasItem($normalizedKey)) { + return false; + } + + return $this->internalSetItem($normalizedKey, $value); + } + + /** + * Replace multiple existing items. + * + * @param array $keyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + * + * @triggers replaceItems.pre(PreEvent) + * @triggers replaceItems.post(PostEvent) + * @triggers replaceItems.exception(ExceptionEvent) + */ + public function replaceItems(array $keyValuePairs) + { + if (!$this->getOptions()->getWritable()) { + return array_keys($keyValuePairs); + } + + $this->normalizeKeyValuePairs($keyValuePairs); + $args = new ArrayObject(array( + 'keyValuePairs' => & $keyValuePairs, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalReplaceItems($args['keyValuePairs']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = array_keys($keyValuePairs); + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to replace multiple existing items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalReplaceItems(array & $normalizedKeyValuePairs) + { + $result = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + if (!$this->internalReplaceItem($normalizedKey, $value)) { + $result[] = $normalizedKey; + } + } + return $result; + } + + /** + * Set an item only if token matches + * + * It uses the token received from getItem() to check if the item has + * changed before overwriting it. + * + * @param mixed $token + * @param string $key + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + * @see getItem() + * @see setItem() + */ + public function checkAndSetItem($token, $key, $value) + { + if (!$this->getOptions()->getWritable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'token' => & $token, + 'key' => & $key, + 'value' => & $value, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalCheckAndSetItem($args['token'], $args['key'], $args['value']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to set an item only if token matches + * + * @param mixed $token + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + * @see getItem() + * @see setItem() + */ + protected function internalCheckAndSetItem(& $token, & $normalizedKey, & $value) + { + $oldValue = $this->internalGetItem($normalizedKey); + if ($oldValue !== $token) { + return false; + } + + return $this->internalSetItem($normalizedKey, $value); + } + + /** + * Reset lifetime of an item + * + * @param string $key + * @return bool + * @throws Exception\ExceptionInterface + * + * @triggers touchItem.pre(PreEvent) + * @triggers touchItem.post(PostEvent) + * @triggers touchItem.exception(ExceptionEvent) + */ + public function touchItem($key) + { + if (!$this->getOptions()->getWritable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'key' => & $key, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalTouchItem($args['key']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to reset lifetime of an item + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalTouchItem(& $normalizedKey) + { + $success = null; + $value = $this->internalGetItem($normalizedKey, $success); + if (!$success) { + return false; + } + + return $this->internalReplaceItem($normalizedKey, $value); + } + + /** + * Reset lifetime of multiple items. + * + * @param array $keys + * @return array Array of not updated keys + * @throws Exception\ExceptionInterface + * + * @triggers touchItems.pre(PreEvent) + * @triggers touchItems.post(PostEvent) + * @triggers touchItems.exception(ExceptionEvent) + */ + public function touchItems(array $keys) + { + if (!$this->getOptions()->getWritable()) { + return $keys; + } + + $this->normalizeKeys($keys); + $args = new ArrayObject(array( + 'keys' => & $keys, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalTouchItems($args['keys']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + return $this->triggerException(__FUNCTION__, $args, $keys, $e); + } + } + + /** + * Internal method to reset lifetime of multiple items. + * + * @param array $normalizedKeys + * @return array Array of not updated keys + * @throws Exception\ExceptionInterface + */ + protected function internalTouchItems(array & $normalizedKeys) + { + $result = array(); + foreach ($normalizedKeys as $normalizedKey) { + if (!$this->internalTouchItem($normalizedKey)) { + $result[] = $normalizedKey; + } + } + return $result; + } + + /** + * Remove an item. + * + * @param string $key + * @return bool + * @throws Exception\ExceptionInterface + * + * @triggers removeItem.pre(PreEvent) + * @triggers removeItem.post(PostEvent) + * @triggers removeItem.exception(ExceptionEvent) + */ + public function removeItem($key) + { + if (!$this->getOptions()->getWritable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'key' => & $key, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalRemoveItem($args['key']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + abstract protected function internalRemoveItem(& $normalizedKey); + + /** + * Remove multiple items. + * + * @param array $keys + * @return array Array of not removed keys + * @throws Exception\ExceptionInterface + * + * @triggers removeItems.pre(PreEvent) + * @triggers removeItems.post(PostEvent) + * @triggers removeItems.exception(ExceptionEvent) + */ + public function removeItems(array $keys) + { + if (!$this->getOptions()->getWritable()) { + return $keys; + } + + $this->normalizeKeys($keys); + $args = new ArrayObject(array( + 'keys' => & $keys, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalRemoveItems($args['keys']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + return $this->triggerException(__FUNCTION__, $args, $keys, $e); + } + } + + /** + * Internal method to remove multiple items. + * + * @param array $normalizedKeys + * @return array Array of not removed keys + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItems(array & $normalizedKeys) + { + $result = array(); + foreach ($normalizedKeys as $normalizedKey) { + if (!$this->internalRemoveItem($normalizedKey)) { + $result[] = $normalizedKey; + } + } + return $result; + } + + /** + * Increment an item. + * + * @param string $key + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + * + * @triggers incrementItem.pre(PreEvent) + * @triggers incrementItem.post(PostEvent) + * @triggers incrementItem.exception(ExceptionEvent) + */ + public function incrementItem($key, $value) + { + if (!$this->getOptions()->getWritable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'key' => & $key, + 'value' => & $value, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalIncrementItem($args['key'], $args['value']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to increment an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalIncrementItem(& $normalizedKey, & $value) + { + $success = null; + $value = (int) $value; + $get = (int) $this->internalGetItem($normalizedKey, $success); + $newValue = $get + $value; + + if ($success) { + $this->internalReplaceItem($normalizedKey, $newValue); + } else { + $this->internalAddItem($normalizedKey, $newValue); + } + + return $newValue; + } + + /** + * Increment multiple items. + * + * @param array $keyValuePairs + * @return array Associative array of keys and new values + * @throws Exception\ExceptionInterface + * + * @triggers incrementItems.pre(PreEvent) + * @triggers incrementItems.post(PostEvent) + * @triggers incrementItems.exception(ExceptionEvent) + */ + public function incrementItems(array $keyValuePairs) + { + if (!$this->getOptions()->getWritable()) { + return array(); + } + + $this->normalizeKeyValuePairs($keyValuePairs); + $args = new ArrayObject(array( + 'keyValuePairs' => & $keyValuePairs, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalIncrementItems($args['keyValuePairs']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = array(); + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to increment multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Associative array of keys and new values + * @throws Exception\ExceptionInterface + */ + protected function internalIncrementItems(array & $normalizedKeyValuePairs) + { + $result = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + $newValue = $this->internalIncrementItem($normalizedKey, $value); + if ($newValue !== false) { + $result[$normalizedKey] = $newValue; + } + } + return $result; + } + + /** + * Decrement an item. + * + * @param string $key + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + * + * @triggers decrementItem.pre(PreEvent) + * @triggers decrementItem.post(PostEvent) + * @triggers decrementItem.exception(ExceptionEvent) + */ + public function decrementItem($key, $value) + { + if (!$this->getOptions()->getWritable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'key' => & $key, + 'value' => & $value, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalDecrementItem($args['key'], $args['value']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to decrement an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalDecrementItem(& $normalizedKey, & $value) + { + $success = null; + $value = (int) $value; + $get = (int) $this->internalGetItem($normalizedKey, $success); + $newValue = $get - $value; + + if ($success) { + $this->internalReplaceItem($normalizedKey, $newValue); + } else { + $this->internalAddItem($normalizedKey, $newValue); + } + + return $newValue; + } + + /** + * Decrement multiple items. + * + * @param array $keyValuePairs + * @return array Associative array of keys and new values + * @throws Exception\ExceptionInterface + * + * @triggers incrementItems.pre(PreEvent) + * @triggers incrementItems.post(PostEvent) + * @triggers incrementItems.exception(ExceptionEvent) + */ + public function decrementItems(array $keyValuePairs) + { + if (!$this->getOptions()->getWritable()) { + return array(); + } + + $this->normalizeKeyValuePairs($keyValuePairs); + $args = new ArrayObject(array( + 'keyValuePairs' => & $keyValuePairs, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalDecrementItems($args['keyValuePairs']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = array(); + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to decrement multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Associative array of keys and new values + * @throws Exception\ExceptionInterface + */ + protected function internalDecrementItems(array & $normalizedKeyValuePairs) + { + $result = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + $newValue = $this->decrementItem($normalizedKey, $value); + if ($newValue !== false) { + $result[$normalizedKey] = $newValue; + } + } + return $result; + } + + /* status */ + + /** + * Get capabilities of this adapter + * + * @return Capabilities + * @triggers getCapabilities.pre(PreEvent) + * @triggers getCapabilities.post(PostEvent) + * @triggers getCapabilities.exception(ExceptionEvent) + */ + public function getCapabilities() + { + $args = new ArrayObject(); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalGetCapabilities(); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities === null) { + $this->capabilityMarker = new stdClass(); + $this->capabilities = new Capabilities($this, $this->capabilityMarker); + } + return $this->capabilities; + } + + /* internal */ + + /** + * Validates and normalizes a key + * + * @param string $key + * @return void + * @throws Exception\InvalidArgumentException On an invalid key + */ + protected function normalizeKey(& $key) + { + $key = (string) $key; + + if ($key === '') { + throw new Exception\InvalidArgumentException( + "An empty key isn't allowed" + ); + } elseif (($p = $this->getOptions()->getKeyPattern()) && !preg_match($p, $key)) { + throw new Exception\InvalidArgumentException( + "The key '{$key}' doesn't match against pattern '{$p}'" + ); + } + } + + /** + * Validates and normalizes multiple keys + * + * @param array $keys + * @return void + * @throws Exception\InvalidArgumentException On an invalid key + */ + protected function normalizeKeys(array & $keys) + { + if (!$keys) { + throw new Exception\InvalidArgumentException( + "An empty list of keys isn't allowed" + ); + } + + array_walk($keys, array($this, 'normalizeKey')); + $keys = array_values(array_unique($keys)); + } + + /** + * Validates and normalizes an array of key-value pairs + * + * @param array $keyValuePairs + * @return void + * @throws Exception\InvalidArgumentException On an invalid key + */ + protected function normalizeKeyValuePairs(array & $keyValuePairs) + { + $normalizedKeyValuePairs = array(); + foreach ($keyValuePairs as $key => $value) { + $this->normalizeKey($key); + $normalizedKeyValuePairs[$key] = $value; + } + $keyValuePairs = $normalizedKeyValuePairs; + } +} diff --git a/library/Zend/Cache/Storage/Adapter/AbstractZendServer.php b/library/Zend/Cache/Storage/Adapter/AbstractZendServer.php new file mode 100755 index 0000000000..22933beb0c --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/AbstractZendServer.php @@ -0,0 +1,273 @@ +getOptions()->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . self::NAMESPACE_SEPARATOR; + + $result = $this->zdcFetch($prefix . $normalizedKey); + if ($result === null) { + $success = false; + } else { + $success = true; + $casToken = $result; + } + + return $result; + } + + /** + * Internal method to get multiple items. + * + * @param array $normalizedKeys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + */ + protected function internalGetItems(array & $normalizedKeys) + { + $namespace = $this->getOptions()->getNamespace(); + if ($namespace === '') { + return $this->zdcFetchMulti($normalizedKeys); + } + + $prefix = $namespace . self::NAMESPACE_SEPARATOR; + $internalKeys = array(); + foreach ($normalizedKeys as $normalizedKey) { + $internalKeys[] = $prefix . $normalizedKey; + } + + $fetch = $this->zdcFetchMulti($internalKeys); + $result = array(); + $prefixL = strlen($prefix); + foreach ($fetch as $k => & $v) { + $result[substr($k, $prefixL)] = $v; + } + + return $result; + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalHasItem(& $normalizedKey) + { + $namespace = $this->getOptions()->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . self::NAMESPACE_SEPARATOR; + return ($this->zdcFetch($prefix . $normalizedKey) !== false); + } + + /** + * Internal method to test multiple items. + * + * @param array $normalizedKeys + * @return array Array of found keys + * @throws Exception\ExceptionInterface + */ + protected function internalHasItems(array & $normalizedKeys) + { + $namespace = $this->getOptions()->getNamespace(); + if ($namespace === '') { + return array_keys($this->zdcFetchMulti($normalizedKeys)); + } + + $prefix = $namespace . self::NAMESPACE_SEPARATOR; + $internalKeys = array(); + foreach ($normalizedKeys as $normalizedKey) { + $internalKeys[] = $prefix . $normalizedKey; + } + + $fetch = $this->zdcFetchMulti($internalKeys); + $result = array(); + $prefixL = strlen($prefix); + foreach ($fetch as $internalKey => & $value) { + $result[] = substr($internalKey, $prefixL); + } + + return $result; + } + + /** + * Get metadata for multiple items + * + * @param array $normalizedKeys + * @return array Associative array of keys and metadata + * + * @triggers getMetadatas.pre(PreEvent) + * @triggers getMetadatas.post(PostEvent) + * @triggers getMetadatas.exception(ExceptionEvent) + */ + protected function internalGetMetadatas(array & $normalizedKeys) + { + $namespace = $this->getOptions()->getNamespace(); + if ($namespace === '') { + $result = $this->zdcFetchMulti($normalizedKeys); + return array_fill_keys(array_keys($result), array()); + } + + $prefix = $namespace . self::NAMESPACE_SEPARATOR; + $internalKeys = array(); + foreach ($normalizedKeys as $normalizedKey) { + $internalKeys[] = $prefix . $normalizedKey; + } + + $fetch = $this->zdcFetchMulti($internalKeys); + $result = array(); + $prefixL = strlen($prefix); + foreach ($fetch as $internalKey => $value) { + $result[substr($internalKey, $prefixL)] = array(); + } + + return $result; + } + + /* writing */ + + /** + * Internal method to store an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . self::NAMESPACE_SEPARATOR; + $this->zdcStore($prefix . $normalizedKey, $value, $options->getTtl()); + return true; + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItem(& $normalizedKey) + { + $namespace = $this->getOptions()->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . self::NAMESPACE_SEPARATOR; + return $this->zdcDelete($prefix . $normalizedKey); + } + + /* status */ + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities === null) { + $this->capabilityMarker = new stdClass(); + $this->capabilities = new Capabilities( + $this, + $this->capabilityMarker, + array( + 'supportedDatatypes' => array( + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => true, + 'object' => 'object', + 'resource' => false, + ), + 'supportedMetadata' => array(), + 'maxTtl' => 0, + 'staticTtl' => true, + 'ttlPrecision' => 1, + 'useRequestTime' => false, + 'expiredRead' => false, + 'maxKeyLength' => 0, + 'namespaceIsPrefix' => true, + 'namespaceSeparator' => self::NAMESPACE_SEPARATOR, + ) + ); + } + + return $this->capabilities; + } + + /* internal wrapper of zend_[disk|shm]_cache_* functions */ + + /** + * Store data into Zend Data Cache (zdc) + * + * @param string $internalKey + * @param mixed $value + * @param int $ttl + * @return void + * @throws Exception\RuntimeException + */ + abstract protected function zdcStore($internalKey, $value, $ttl); + + /** + * Fetch a single item from Zend Data Cache (zdc) + * + * @param string $internalKey + * @return mixed The stored value or FALSE if item wasn't found + * @throws Exception\RuntimeException + */ + abstract protected function zdcFetch($internalKey); + + /** + * Fetch multiple items from Zend Data Cache (zdc) + * + * @param array $internalKeys + * @return array All found items + * @throws Exception\RuntimeException + */ + abstract protected function zdcFetchMulti(array $internalKeys); + + /** + * Delete data from Zend Data Cache (zdc) + * + * @param string $internalKey + * @return bool + * @throws Exception\RuntimeException + */ + abstract protected function zdcDelete($internalKey); +} diff --git a/library/Zend/Cache/Storage/Adapter/AdapterOptions.php b/library/Zend/Cache/Storage/Adapter/AdapterOptions.php new file mode 100755 index 0000000000..b3669ffa4a --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/AdapterOptions.php @@ -0,0 +1,264 @@ +adapter = $adapter; + return $this; + } + + /** + * Set key pattern + * + * @param null|string $keyPattern + * @throws Exception\InvalidArgumentException + * @return AdapterOptions + */ + public function setKeyPattern($keyPattern) + { + $keyPattern = (string) $keyPattern; + if ($this->keyPattern !== $keyPattern) { + // validate pattern + if ($keyPattern !== '') { + ErrorHandler::start(E_WARNING); + $result = preg_match($keyPattern, ''); + $error = ErrorHandler::stop(); + if ($result === false) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid pattern "%s"%s', + $keyPattern, + ($error ? ': ' . $error->getMessage() : '') + ), 0, $error); + } + } + + $this->triggerOptionEvent('key_pattern', $keyPattern); + $this->keyPattern = $keyPattern; + } + + return $this; + } + + /** + * Get key pattern + * + * @return string + */ + public function getKeyPattern() + { + return $this->keyPattern; + } + + /** + * Set namespace. + * + * @param string $namespace + * @return AdapterOptions + */ + public function setNamespace($namespace) + { + $namespace = (string) $namespace; + if ($this->namespace !== $namespace) { + $this->triggerOptionEvent('namespace', $namespace); + $this->namespace = $namespace; + } + + return $this; + } + + /** + * Get namespace + * + * @return string + */ + public function getNamespace() + { + return $this->namespace; + } + + /** + * Enable/Disable reading data from cache. + * + * @param bool $readable + * @return AbstractAdapter + */ + public function setReadable($readable) + { + $readable = (bool) $readable; + if ($this->readable !== $readable) { + $this->triggerOptionEvent('readable', $readable); + $this->readable = $readable; + } + return $this; + } + + /** + * If reading data from cache enabled. + * + * @return bool + */ + public function getReadable() + { + return $this->readable; + } + + /** + * Set time to live. + * + * @param int|float $ttl + * @return AdapterOptions + */ + public function setTtl($ttl) + { + $this->normalizeTtl($ttl); + if ($this->ttl !== $ttl) { + $this->triggerOptionEvent('ttl', $ttl); + $this->ttl = $ttl; + } + return $this; + } + + /** + * Get time to live. + * + * @return float + */ + public function getTtl() + { + return $this->ttl; + } + + /** + * Enable/Disable writing data to cache. + * + * @param bool $writable + * @return AdapterOptions + */ + public function setWritable($writable) + { + $writable = (bool) $writable; + if ($this->writable !== $writable) { + $this->triggerOptionEvent('writable', $writable); + $this->writable = $writable; + } + return $this; + } + + /** + * If writing data to cache enabled. + * + * @return bool + */ + public function getWritable() + { + return $this->writable; + } + + /** + * Triggers an option event if this options instance has a connection to + * an adapter implements EventsCapableInterface. + * + * @param string $optionName + * @param mixed $optionValue + * @return void + */ + protected function triggerOptionEvent($optionName, $optionValue) + { + if ($this->adapter instanceof EventsCapableInterface) { + $event = new Event('option', $this->adapter, new ArrayObject(array($optionName => $optionValue))); + $this->adapter->getEventManager()->trigger($event); + } + } + + /** + * Validates and normalize a TTL. + * + * @param int|float $ttl + * @throws Exception\InvalidArgumentException + * @return void + */ + protected function normalizeTtl(&$ttl) + { + if (!is_int($ttl)) { + $ttl = (float) $ttl; + + // convert to int if possible + if ($ttl === (float) (int) $ttl) { + $ttl = (int) $ttl; + } + } + + if ($ttl < 0) { + throw new Exception\InvalidArgumentException("TTL can't be negative"); + } + } +} diff --git a/library/Zend/Cache/Storage/Adapter/Apc.php b/library/Zend/Cache/Storage/Adapter/Apc.php new file mode 100755 index 0000000000..c2336e96ac --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/Apc.php @@ -0,0 +1,751 @@ + 0) { + throw new Exception\ExtensionNotLoadedException("Missing ext/apc >= 3.1.6"); + } + + $enabled = ini_get('apc.enabled'); + if (PHP_SAPI == 'cli') { + $enabled = $enabled && (bool) ini_get('apc.enable_cli'); + } + + if (!$enabled) { + throw new Exception\ExtensionNotLoadedException( + "ext/apc is disabled - see 'apc.enabled' and 'apc.enable_cli'" + ); + } + + parent::__construct($options); + } + + /* options */ + + /** + * Set options. + * + * @param array|Traversable|ApcOptions $options + * @return Apc + * @see getOptions() + */ + public function setOptions($options) + { + if (!$options instanceof ApcOptions) { + $options = new ApcOptions($options); + } + + return parent::setOptions($options); + } + + /** + * Get options. + * + * @return ApcOptions + * @see setOptions() + */ + public function getOptions() + { + if (!$this->options) { + $this->setOptions(new ApcOptions()); + } + return $this->options; + } + + /* TotalSpaceCapableInterface */ + + /** + * Get total space in bytes + * + * @return int|float + */ + public function getTotalSpace() + { + if ($this->totalSpace === null) { + $smaInfo = apc_sma_info(true); + $this->totalSpace = $smaInfo['num_seg'] * $smaInfo['seg_size']; + } + + return $this->totalSpace; + } + + /* AvailableSpaceCapableInterface */ + + /** + * Get available space in bytes + * + * @return int|float + */ + public function getAvailableSpace() + { + $smaInfo = apc_sma_info(true); + return $smaInfo['avail_mem']; + } + + /* IterableInterface */ + + /** + * Get the storage iterator + * + * @return ApcIterator + */ + public function getIterator() + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ''; + $pattern = null; + if ($namespace !== '') { + $prefix = $namespace . $options->getNamespaceSeparator(); + $pattern = '/^' . preg_quote($prefix, '/') . '/'; + } + + $baseIt = new BaseApcIterator('user', $pattern, 0, 1, APC_LIST_ACTIVE); + return new ApcIterator($this, $baseIt, $prefix); + } + + /* FlushableInterface */ + + /** + * Flush the whole storage + * + * @return bool + */ + public function flush() + { + return apc_clear_cache('user'); + } + + /* ClearByNamespaceInterface */ + + /** + * Remove items by given namespace + * + * @param string $namespace + * @return bool + */ + public function clearByNamespace($namespace) + { + $namespace = (string) $namespace; + if ($namespace === '') { + throw new Exception\InvalidArgumentException('No namespace given'); + } + + $options = $this->getOptions(); + $prefix = $namespace . $options->getNamespaceSeparator(); + $pattern = '/^' . preg_quote($prefix, '/') . '/'; + return apc_delete(new BaseApcIterator('user', $pattern, 0, 1, APC_LIST_ACTIVE)); + } + + /* ClearByPrefixInterface */ + + /** + * Remove items matching given prefix + * + * @param string $prefix + * @return bool + */ + public function clearByPrefix($prefix) + { + $prefix = (string) $prefix; + if ($prefix === '') { + throw new Exception\InvalidArgumentException('No prefix given'); + } + + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $nsPrefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $pattern = '/^' . preg_quote($nsPrefix . $prefix, '/') . '/'; + return apc_delete(new BaseApcIterator('user', $pattern, 0, 1, APC_LIST_ACTIVE)); + } + + /* reading */ + + /** + * Internal method to get an item. + * + * @param string $normalizedKey + * @param bool $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $result = apc_fetch($internalKey, $success); + + if (!$success) { + return null; + } + + $casToken = $result; + return $result; + } + + /** + * Internal method to get multiple items. + * + * @param array $normalizedKeys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + */ + protected function internalGetItems(array & $normalizedKeys) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + if ($namespace === '') { + return apc_fetch($normalizedKeys); + } + + $prefix = $namespace . $options->getNamespaceSeparator(); + $internalKeys = array(); + foreach ($normalizedKeys as $normalizedKey) { + $internalKeys[] = $prefix . $normalizedKey; + } + + $fetch = apc_fetch($internalKeys); + + // remove namespace prefix + $prefixL = strlen($prefix); + $result = array(); + foreach ($fetch as $internalKey => & $value) { + $result[substr($internalKey, $prefixL)] = $value; + } + + return $result; + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalHasItem(& $normalizedKey) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + return apc_exists($prefix . $normalizedKey); + } + + /** + * Internal method to test multiple items. + * + * @param array $normalizedKeys + * @return array Array of found keys + * @throws Exception\ExceptionInterface + */ + protected function internalHasItems(array & $normalizedKeys) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + if ($namespace === '') { + // array_filter with no callback will remove entries equal to FALSE + return array_keys(array_filter(apc_exists($normalizedKeys))); + } + + $prefix = $namespace . $options->getNamespaceSeparator(); + $internalKeys = array(); + foreach ($normalizedKeys as $normalizedKey) { + $internalKeys[] = $prefix . $normalizedKey; + } + + $exists = apc_exists($internalKeys); + $result = array(); + $prefixL = strlen($prefix); + foreach ($exists as $internalKey => $bool) { + if ($bool === true) { + $result[] = substr($internalKey, $prefixL); + } + } + + return $result; + } + + /** + * Get metadata of an item. + * + * @param string $normalizedKey + * @return array|bool Metadata on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetMetadata(& $normalizedKey) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + + // @see http://pecl.php.net/bugs/bug.php?id=22564 + if (!apc_exists($internalKey)) { + $metadata = false; + } else { + $format = APC_ITER_ALL ^ APC_ITER_VALUE ^ APC_ITER_TYPE ^ APC_ITER_REFCOUNT; + $regexp = '/^' . preg_quote($internalKey, '/') . '$/'; + $it = new BaseApcIterator('user', $regexp, $format, 100, APC_LIST_ACTIVE); + $metadata = $it->current(); + } + + if (!$metadata) { + return false; + } + + $this->normalizeMetadata($metadata); + return $metadata; + } + + /** + * Get metadata of multiple items + * + * @param array $normalizedKeys + * @return array Associative array of keys and metadata + * + * @triggers getMetadatas.pre(PreEvent) + * @triggers getMetadatas.post(PostEvent) + * @triggers getMetadatas.exception(ExceptionEvent) + */ + protected function internalGetMetadatas(array & $normalizedKeys) + { + $keysRegExp = array(); + foreach ($normalizedKeys as $normalizedKey) { + $keysRegExp[] = preg_quote($normalizedKey, '/'); + } + + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + if ($namespace === '') { + $pattern = '/^(' . implode('|', $keysRegExp) . ')' . '$/'; + } else { + $prefix = $namespace . $options->getNamespaceSeparator(); + $pattern = '/^' . preg_quote($prefix, '/') . '(' . implode('|', $keysRegExp) . ')' . '$/'; + } + $format = APC_ITER_ALL ^ APC_ITER_VALUE ^ APC_ITER_TYPE ^ APC_ITER_REFCOUNT; + $it = new BaseApcIterator('user', $pattern, $format, 100, APC_LIST_ACTIVE); + $result = array(); + $prefixL = strlen($prefix); + foreach ($it as $internalKey => $metadata) { + // @see http://pecl.php.net/bugs/bug.php?id=22564 + if (!apc_exists($internalKey)) { + continue; + } + + $this->normalizeMetadata($metadata); + $result[substr($internalKey, $prefixL)] = & $metadata; + } + + return $result; + } + + /* writing */ + + /** + * Internal method to store an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $ttl = $options->getTtl(); + + if (!apc_store($internalKey, $value, $ttl)) { + $type = is_object($value) ? get_class($value) : gettype($value); + throw new Exception\RuntimeException( + "apc_store('{$internalKey}', <{$type}>, {$ttl}) failed" + ); + } + + return true; + } + + /** + * Internal method to store multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalSetItems(array & $normalizedKeyValuePairs) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + if ($namespace === '') { + return array_keys(apc_store($normalizedKeyValuePairs, null, $options->getTtl())); + } + + $prefix = $namespace . $options->getNamespaceSeparator(); + $internalKeyValuePairs = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => &$value) { + $internalKey = $prefix . $normalizedKey; + $internalKeyValuePairs[$internalKey] = &$value; + } + + $failedKeys = apc_store($internalKeyValuePairs, null, $options->getTtl()); + $failedKeys = array_keys($failedKeys); + + // remove prefix + $prefixL = strlen($prefix); + foreach ($failedKeys as & $key) { + $key = substr($key, $prefixL); + } + + return $failedKeys; + } + + /** + * Add an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalAddItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $ttl = $options->getTtl(); + + if (!apc_add($internalKey, $value, $ttl)) { + if (apc_exists($internalKey)) { + return false; + } + + $type = is_object($value) ? get_class($value) : gettype($value); + throw new Exception\RuntimeException( + "apc_add('{$internalKey}', <{$type}>, {$ttl}) failed" + ); + } + + return true; + } + + /** + * Internal method to add multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalAddItems(array & $normalizedKeyValuePairs) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + if ($namespace === '') { + return array_keys(apc_add($normalizedKeyValuePairs, null, $options->getTtl())); + } + + $prefix = $namespace . $options->getNamespaceSeparator(); + $internalKeyValuePairs = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + $internalKey = $prefix . $normalizedKey; + $internalKeyValuePairs[$internalKey] = $value; + } + + $failedKeys = apc_add($internalKeyValuePairs, null, $options->getTtl()); + $failedKeys = array_keys($failedKeys); + + // remove prefix + $prefixL = strlen($prefix); + foreach ($failedKeys as & $key) { + $key = substr($key, $prefixL); + } + + return $failedKeys; + } + + /** + * Internal method to replace an existing item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalReplaceItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + + if (!apc_exists($internalKey)) { + return false; + } + + $ttl = $options->getTtl(); + if (!apc_store($internalKey, $value, $ttl)) { + $type = is_object($value) ? get_class($value) : gettype($value); + throw new Exception\RuntimeException( + "apc_store('{$internalKey}', <{$type}>, {$ttl}) failed" + ); + } + + return true; + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItem(& $normalizedKey) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + return apc_delete($prefix . $normalizedKey); + } + + /** + * Internal method to remove multiple items. + * + * @param array $normalizedKeys + * @return array Array of not removed keys + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItems(array & $normalizedKeys) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + if ($namespace === '') { + return apc_delete($normalizedKeys); + } + + $prefix = $namespace . $options->getNamespaceSeparator(); + $internalKeys = array(); + foreach ($normalizedKeys as $normalizedKey) { + $internalKeys[] = $prefix . $normalizedKey; + } + + $failedKeys = apc_delete($internalKeys); + + // remove prefix + $prefixL = strlen($prefix); + foreach ($failedKeys as & $key) { + $key = substr($key, $prefixL); + } + + return $failedKeys; + } + + /** + * Internal method to increment an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalIncrementItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $value = (int) $value; + $newValue = apc_inc($internalKey, $value); + + // initial value + if ($newValue === false) { + $ttl = $options->getTtl(); + $newValue = $value; + if (!apc_add($internalKey, $newValue, $ttl)) { + throw new Exception\RuntimeException( + "apc_add('{$internalKey}', {$newValue}, {$ttl}) failed" + ); + } + } + + return $newValue; + } + + /** + * Internal method to decrement an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalDecrementItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $value = (int) $value; + $newValue = apc_dec($internalKey, $value); + + // initial value + if ($newValue === false) { + $ttl = $options->getTtl(); + $newValue = -$value; + if (!apc_add($internalKey, $newValue, $ttl)) { + throw new Exception\RuntimeException( + "apc_add('{$internalKey}', {$newValue}, {$ttl}) failed" + ); + } + } + + return $newValue; + } + + /* status */ + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities === null) { + $marker = new stdClass(); + $capabilities = new Capabilities( + $this, + $marker, + array( + 'supportedDatatypes' => array( + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => true, + 'object' => 'object', + 'resource' => false, + ), + 'supportedMetadata' => array( + 'internal_key', + 'atime', 'ctime', 'mtime', 'rtime', + 'size', 'hits', 'ttl', + ), + 'minTtl' => 1, + 'maxTtl' => 0, + 'staticTtl' => true, + 'ttlPrecision' => 1, + 'useRequestTime' => (bool) ini_get('apc.use_request_time'), + 'expiredRead' => false, + 'maxKeyLength' => 5182, + 'namespaceIsPrefix' => true, + 'namespaceSeparator' => $this->getOptions()->getNamespaceSeparator(), + ) + ); + + // update namespace separator on change option + $this->getEventManager()->attach('option', function ($event) use ($capabilities, $marker) { + $params = $event->getParams(); + + if (isset($params['namespace_separator'])) { + $capabilities->setNamespaceSeparator($marker, $params['namespace_separator']); + } + }); + + $this->capabilities = $capabilities; + $this->capabilityMarker = $marker; + } + + return $this->capabilities; + } + + /* internal */ + + /** + * Normalize metadata to work with APC + * + * @param array $metadata + * @return void + */ + protected function normalizeMetadata(array & $metadata) + { + $metadata['internal_key'] = $metadata['key']; + $metadata['ctime'] = $metadata['creation_time']; + $metadata['atime'] = $metadata['access_time']; + $metadata['rtime'] = $metadata['deletion_time']; + $metadata['size'] = $metadata['mem_size']; + $metadata['hits'] = $metadata['num_hits']; + + unset( + $metadata['key'], + $metadata['creation_time'], + $metadata['access_time'], + $metadata['deletion_time'], + $metadata['mem_size'], + $metadata['num_hits'] + ); + } + + /** + * Internal method to set an item only if token matches + * + * @param mixed $token + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @see getItem() + * @see setItem() + */ + protected function internalCheckAndSetItem(& $token, & $normalizedKey, & $value) + { + return apc_cas($normalizedKey, $token, $value); + } +} diff --git a/library/Zend/Cache/Storage/Adapter/ApcIterator.php b/library/Zend/Cache/Storage/Adapter/ApcIterator.php new file mode 100755 index 0000000000..3cdcbf1c55 --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/ApcIterator.php @@ -0,0 +1,157 @@ +storage = $storage; + $this->baseIterator = $baseIterator; + $this->prefixLength = strlen($prefix); + } + + /** + * Get storage instance + * + * @return Apc + */ + public function getStorage() + { + return $this->storage; + } + + /** + * Get iterator mode + * + * @return int Value of IteratorInterface::CURRENT_AS_* + */ + public function getMode() + { + return $this->mode; + } + + /** + * Set iterator mode + * + * @param int $mode + * @return ApcIterator Fluent interface + */ + public function setMode($mode) + { + $this->mode = (int) $mode; + return $this; + } + + /* Iterator */ + + /** + * Get current key, value or metadata. + * + * @return mixed + */ + public function current() + { + if ($this->mode == IteratorInterface::CURRENT_AS_SELF) { + return $this; + } + + $key = $this->key(); + + if ($this->mode == IteratorInterface::CURRENT_AS_VALUE) { + return $this->storage->getItem($key); + } elseif ($this->mode == IteratorInterface::CURRENT_AS_METADATA) { + return $this->storage->getMetadata($key); + } + + return $key; + } + + /** + * Get current key + * + * @return string + */ + public function key() + { + $key = $this->baseIterator->key(); + + // remove namespace prefix + return substr($key, $this->prefixLength); + } + + /** + * Move forward to next element + * + * @return void + */ + public function next() + { + $this->baseIterator->next(); + } + + /** + * Checks if current position is valid + * + * @return bool + */ + public function valid() + { + return $this->baseIterator->valid(); + } + + /** + * Rewind the Iterator to the first element. + * + * @return void + */ + public function rewind() + { + return $this->baseIterator->rewind(); + } +} diff --git a/library/Zend/Cache/Storage/Adapter/ApcOptions.php b/library/Zend/Cache/Storage/Adapter/ApcOptions.php new file mode 100755 index 0000000000..0299d9446c --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/ApcOptions.php @@ -0,0 +1,47 @@ +triggerOptionEvent('namespace_separator', $namespaceSeparator); + $this->namespaceSeparator = $namespaceSeparator; + return $this; + } + + /** + * Get namespace separator + * + * @return string + */ + public function getNamespaceSeparator() + { + return $this->namespaceSeparator; + } +} diff --git a/library/Zend/Cache/Storage/Adapter/BlackHole.php b/library/Zend/Cache/Storage/Adapter/BlackHole.php new file mode 100755 index 0000000000..2938cfdeed --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/BlackHole.php @@ -0,0 +1,502 @@ +setOptions($options); + } + } + + /** + * Set options. + * + * @param array|\Traversable|AdapterOptions $options + * @return StorageInterface Fluent interface + */ + public function setOptions($options) + { + if ($this->options !== $options) { + if (!$options instanceof AdapterOptions) { + $options = new AdapterOptions($options); + } + + if ($this->options) { + $this->options->setAdapter(null); + } + $options->setAdapter($this); + $this->options = $options; + } + return $this; + } + + /** + * Get options + * + * @return AdapterOptions + */ + public function getOptions() + { + if (!$this->options) { + $this->setOptions(new AdapterOptions()); + } + return $this->options; + } + + /** + * Get an item. + * + * @param string $key + * @param bool $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + */ + public function getItem($key, & $success = null, & $casToken = null) + { + $success = false; + return null; + } + + /** + * Get multiple items. + * + * @param array $keys + * @return array Associative array of keys and values + */ + public function getItems(array $keys) + { + return array(); + } + + /** + * Test if an item exists. + * + * @param string $key + * @return bool + */ + public function hasItem($key) + { + return false; + } + + /** + * Test multiple items. + * + * @param array $keys + * @return array Array of found keys + */ + public function hasItems(array $keys) + { + return array(); + } + + /** + * Get metadata of an item. + * + * @param string $key + * @return array|bool Metadata on success, false on failure + */ + public function getMetadata($key) + { + return false; + } + + /** + * Get multiple metadata + * + * @param array $keys + * @return array Associative array of keys and metadata + */ + public function getMetadatas(array $keys) + { + return array(); + } + + /** + * Store an item. + * + * @param string $key + * @param mixed $value + * @return bool + */ + public function setItem($key, $value) + { + return false; + } + + /** + * Store multiple items. + * + * @param array $keyValuePairs + * @return array Array of not stored keys + */ + public function setItems(array $keyValuePairs) + { + return array_keys($keyValuePairs); + } + + /** + * Add an item. + * + * @param string $key + * @param mixed $value + * @return bool + */ + public function addItem($key, $value) + { + return false; + } + + /** + * Add multiple items. + * + * @param array $keyValuePairs + * @return array Array of not stored keys + */ + public function addItems(array $keyValuePairs) + { + return array_keys($keyValuePairs); + } + + /** + * Replace an existing item. + * + * @param string $key + * @param mixed $value + * @return bool + */ + public function replaceItem($key, $value) + { + return false; + } + + /** + * Replace multiple existing items. + * + * @param array $keyValuePairs + * @return array Array of not stored keys + */ + public function replaceItems(array $keyValuePairs) + { + return array_keys($keyValuePairs); + } + + /** + * Set an item only if token matches + * + * It uses the token received from getItem() to check if the item has + * changed before overwriting it. + * + * @param mixed $token + * @param string $key + * @param mixed $value + * @return bool + */ + public function checkAndSetItem($token, $key, $value) + { + return false; + } + + /** + * Reset lifetime of an item + * + * @param string $key + * @return bool + */ + public function touchItem($key) + { + return false; + } + + /** + * Reset lifetime of multiple items. + * + * @param array $keys + * @return array Array of not updated keys + */ + public function touchItems(array $keys) + { + return $keys; + } + + /** + * Remove an item. + * + * @param string $key + * @return bool + */ + public function removeItem($key) + { + return false; + } + + /** + * Remove multiple items. + * + * @param array $keys + * @return array Array of not removed keys + */ + public function removeItems(array $keys) + { + return $keys; + } + + /** + * Increment an item. + * + * @param string $key + * @param int $value + * @return int|bool The new value on success, false on failure + */ + public function incrementItem($key, $value) + { + return false; + } + + /** + * Increment multiple items. + * + * @param array $keyValuePairs + * @return array Associative array of keys and new values + */ + public function incrementItems(array $keyValuePairs) + { + return array(); + } + + /** + * Decrement an item. + * + * @param string $key + * @param int $value + * @return int|bool The new value on success, false on failure + */ + public function decrementItem($key, $value) + { + return false; + } + + /** + * Decrement multiple items. + * + * @param array $keyValuePairs + * @return array Associative array of keys and new values + */ + public function decrementItems(array $keyValuePairs) + { + return array(); + } + + /** + * Capabilities of this storage + * + * @return Capabilities + */ + public function getCapabilities() + { + if ($this->capabilities === null) { + // use default capabilities only + $this->capabilityMarker = new stdClass(); + $this->capabilities = new Capabilities($this, $this->capabilityMarker); + } + return $this->capabilities; + } + + /* AvailableSpaceCapableInterface */ + + /** + * Get available space in bytes + * + * @return int|float + */ + public function getAvailableSpace() + { + return 0; + } + + /* ClearByNamespaceInterface */ + + /** + * Remove items of given namespace + * + * @param string $namespace + * @return bool + */ + public function clearByNamespace($namespace) + { + return false; + } + + /* ClearByPrefixInterface */ + + /** + * Remove items matching given prefix + * + * @param string $prefix + * @return bool + */ + public function clearByPrefix($prefix) + { + return false; + } + + /* ClearExpiredInterface */ + + /** + * Remove expired items + * + * @return bool + */ + public function clearExpired() + { + return false; + } + + /* FlushableInterface */ + + /** + * Flush the whole storage + * + * @return bool + */ + public function flush() + { + return false; + } + + /* IterableInterface */ + + /** + * Get the storage iterator + * + * @return KeyListIterator + */ + public function getIterator() + { + return new KeyListIterator($this, array()); + } + + /* OptimizableInterface */ + + /** + * Optimize the storage + * + * @return bool + */ + public function optimize() + { + return false; + } + + /* TaggableInterface */ + + /** + * Set tags to an item by given key. + * An empty array will remove all tags. + * + * @param string $key + * @param string[] $tags + * @return bool + */ + public function setTags($key, array $tags) + { + return false; + } + + /** + * Get tags of an item by given key + * + * @param string $key + * @return string[]|FALSE + */ + public function getTags($key) + { + return false; + } + + /** + * Remove items matching given tags. + * + * If $disjunction only one of the given tags must match + * else all given tags must match. + * + * @param string[] $tags + * @param bool $disjunction + * @return bool + */ + public function clearByTags(array $tags, $disjunction = false) + { + return false; + } + + /* TotalSpaceCapableInterface */ + + /** + * Get total space in bytes + * + * @return int|float + */ + public function getTotalSpace() + { + return 0; + } +} diff --git a/library/Zend/Cache/Storage/Adapter/Dba.php b/library/Zend/Cache/Storage/Adapter/Dba.php new file mode 100755 index 0000000000..bb2860d7c0 --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/Dba.php @@ -0,0 +1,541 @@ +_close(); + + parent::__destruct(); + } + + /* options */ + + /** + * Set options. + * + * @param array|Traversable|DbaOptions $options + * @return Apc + * @see getOptions() + */ + public function setOptions($options) + { + if (!$options instanceof DbaOptions) { + $options = new DbaOptions($options); + } + + return parent::setOptions($options); + } + + /** + * Get options. + * + * @return DbaOptions + * @see setOptions() + */ + public function getOptions() + { + if (!$this->options) { + $this->setOptions(new DbaOptions()); + } + return $this->options; + } + + /* TotalSpaceCapableInterface */ + + /** + * Get total space in bytes + * + * @return int|float + */ + public function getTotalSpace() + { + if ($this->totalSpace === null) { + $pathname = $this->getOptions()->getPathname(); + + if ($pathname === '') { + throw new Exception\LogicException('No pathname to database file'); + } + + ErrorHandler::start(); + $total = disk_total_space(dirname($pathname)); + $error = ErrorHandler::stop(); + if ($total === false) { + throw new Exception\RuntimeException("Can't detect total space of '{$pathname}'", 0, $error); + } + $this->totalSpace = $total; + + // clean total space buffer on change pathname + $events = $this->getEventManager(); + $handle = null; + $totalSpace = & $this->totalSpace; + $callback = function ($event) use (& $events, & $handle, & $totalSpace) { + $params = $event->getParams(); + if (isset($params['pathname'])) { + $totalSpace = null; + $events->detach($handle); + } + }; + $events->attach('option', $callback); + } + + return $this->totalSpace; + } + + /* AvailableSpaceCapableInterface */ + + /** + * Get available space in bytes + * + * @return int|float + */ + public function getAvailableSpace() + { + $pathname = $this->getOptions()->getPathname(); + + if ($pathname === '') { + throw new Exception\LogicException('No pathname to database file'); + } + + ErrorHandler::start(); + $avail = disk_free_space(dirname($pathname)); + $error = ErrorHandler::stop(); + if ($avail === false) { + throw new Exception\RuntimeException("Can't detect free space of '{$pathname}'", 0, $error); + } + + return $avail; + } + + /* FlushableInterface */ + + /** + * Flush the whole storage + * + * @return bool + */ + public function flush() + { + $pathname = $this->getOptions()->getPathname(); + + if ($pathname === '') { + throw new Exception\LogicException('No pathname to database file'); + } + + if (file_exists($pathname)) { + // close the dba file before delete + // and reopen (create) on next use + $this->_close(); + + ErrorHandler::start(); + $result = unlink($pathname); + $error = ErrorHandler::stop(); + if (!$result) { + throw new Exception\RuntimeException("unlink('{$pathname}') failed", 0, $error); + } + } + + return true; + } + + /* ClearByNamespaceInterface */ + + /** + * Remove items by given namespace + * + * @param string $namespace + * @return bool + */ + public function clearByNamespace($namespace) + { + $namespace = (string) $namespace; + if ($namespace === '') { + throw new Exception\InvalidArgumentException('No namespace given'); + } + + $prefix = $namespace . $this->getOptions()->getNamespaceSeparator(); + $prefixl = strlen($prefix); + $result = true; + + $this->_open(); + + do { // Workaround for PHP-Bug #62491 & #62492 + $recheck = false; + $internalKey = dba_firstkey($this->handle); + while ($internalKey !== false && $internalKey !== null) { + if (substr($internalKey, 0, $prefixl) === $prefix) { + $result = dba_delete($internalKey, $this->handle) && $result; + } + + $internalKey = dba_nextkey($this->handle); + } + } while ($recheck); + + return $result; + } + + /* ClearByPrefixInterface */ + + /** + * Remove items matching given prefix + * + * @param string $prefix + * @return bool + */ + public function clearByPrefix($prefix) + { + $prefix = (string) $prefix; + if ($prefix === '') { + throw new Exception\InvalidArgumentException('No prefix given'); + } + + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator() . $prefix; + $prefixL = strlen($prefix); + $result = true; + + $this->_open(); + + do { // Workaround for PHP-Bug #62491 & #62492 + $recheck = false; + $internalKey = dba_firstkey($this->handle); + while ($internalKey !== false && $internalKey !== null) { + if (substr($internalKey, 0, $prefixL) === $prefix) { + $result = dba_delete($internalKey, $this->handle) && $result; + $recheck = true; + } + + $internalKey = dba_nextkey($this->handle); + } + } while ($recheck); + + return $result; + } + + /* IterableInterface */ + + /** + * Get the storage iterator + * + * @return ApcIterator + */ + public function getIterator() + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + + return new DbaIterator($this, $this->handle, $prefix); + } + + /* OptimizableInterface */ + + /** + * Optimize the storage + * + * @return bool + * @return Exception\RuntimeException + */ + public function optimize() + { + $this->_open(); + if (!dba_optimize($this->handle)) { + throw new Exception\RuntimeException('dba_optimize failed'); + } + return true; + } + + /* reading */ + + /** + * Internal method to get an item. + * + * @param string $normalizedKey + * @param bool $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + + $this->_open(); + $value = dba_fetch($prefix . $normalizedKey, $this->handle); + + if ($value === false) { + $success = false; + return null; + } + + $success = true; + $casToken = $value; + return $value; + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalHasItem(& $normalizedKey) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + + $this->_open(); + return dba_exists($prefix . $normalizedKey, $this->handle); + } + + /* writing */ + + /** + * Internal method to store an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + + $this->_open(); + if (!dba_replace($internalKey, $value, $this->handle)) { + throw new Exception\RuntimeException("dba_replace('{$internalKey}', ...) failed"); + } + + return true; + } + + /** + * Add an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalAddItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + + $this->_open(); + + // Workaround for PHP-Bug #54242 & #62489 + if (dba_exists($internalKey, $this->handle)) { + return false; + } + + // Workaround for PHP-Bug #54242 & #62489 + // dba_insert returns true if key already exists + ErrorHandler::start(); + $result = dba_insert($internalKey, $value, $this->handle); + $error = ErrorHandler::stop(); + if (!$result || $error) { + return false; + } + + return true; + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItem(& $normalizedKey) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + + $this->_open(); + + // Workaround for PHP-Bug #62490 + if (!dba_exists($internalKey, $this->handle)) { + return false; + } + + return dba_delete($internalKey, $this->handle); + } + + /* status */ + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities === null) { + $marker = new stdClass(); + $capabilities = new Capabilities( + $this, + $marker, + array( + 'supportedDatatypes' => array( + 'NULL' => 'string', + 'boolean' => 'string', + 'integer' => 'string', + 'double' => 'string', + 'string' => true, + 'array' => false, + 'object' => false, + 'resource' => false, + ), + 'minTtl' => 0, + 'supportedMetadata' => array(), + 'maxKeyLength' => 0, // TODO: maxKeyLength ???? + 'namespaceIsPrefix' => true, + 'namespaceSeparator' => $this->getOptions()->getNamespaceSeparator(), + ) + ); + + // update namespace separator on change option + $this->getEventManager()->attach('option', function ($event) use ($capabilities, $marker) { + $params = $event->getParams(); + + if (isset($params['namespace_separator'])) { + $capabilities->setNamespaceSeparator($marker, $params['namespace_separator']); + } + }); + + $this->capabilities = $capabilities; + $this->capabilityMarker = $marker; + } + + return $this->capabilities; + } + + /** + * Open the database if not already done. + * + * @return void + * @throws Exception\LogicException + * @throws Exception\RuntimeException + */ + protected function _open() + { + if (!$this->handle) { + $options = $this->getOptions(); + $pathname = $options->getPathname(); + $mode = $options->getMode(); + $handler = $options->getHandler(); + + if ($pathname === '') { + throw new Exception\LogicException('No pathname to database file'); + } + + ErrorHandler::start(); + $dba = dba_open($pathname, $mode, $handler); + $err = ErrorHandler::stop(); + if (!$dba) { + throw new Exception\RuntimeException( + "dba_open('{$pathname}', '{$mode}', '{$handler}') failed", + 0, + $err + ); + } + $this->handle = $dba; + } + } + + /** + * Close database file if opened + * + * @return void + */ + protected function _close() + { + if ($this->handle) { + ErrorHandler::start(E_NOTICE); + dba_close($this->handle); + ErrorHandler::stop(); + $this->handle = null; + } + } +} diff --git a/library/Zend/Cache/Storage/Adapter/DbaIterator.php b/library/Zend/Cache/Storage/Adapter/DbaIterator.php new file mode 100755 index 0000000000..a895ce545b --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/DbaIterator.php @@ -0,0 +1,190 @@ +storage = $storage; + $this->handle = $handle; + $this->prefixLength = strlen($prefix); + + $this->rewind(); + } + + /** + * Get storage instance + * + * @return Dba + */ + public function getStorage() + { + return $this->storage; + } + + /** + * Get iterator mode + * + * @return int Value of IteratorInterface::CURRENT_AS_* + */ + public function getMode() + { + return $this->mode; + } + + /** + * Set iterator mode + * + * @param int $mode + * @return ApcIterator Fluent interface + */ + public function setMode($mode) + { + $this->mode = (int) $mode; + return $this; + } + + /* Iterator */ + + /** + * Get current key, value or metadata. + * + * @return mixed + * @throws Exception\RuntimeException + */ + public function current() + { + if ($this->mode == IteratorInterface::CURRENT_AS_SELF) { + return $this; + } + + $key = $this->key(); + + if ($this->mode == IteratorInterface::CURRENT_AS_VALUE) { + return $this->storage->getItem($key); + } elseif ($this->mode == IteratorInterface::CURRENT_AS_METADATA) { + return $this->storage->getMetadata($key); + } + + return $key; + } + + /** + * Get current key + * + * @return string + * @throws Exception\RuntimeException + */ + public function key() + { + if ($this->currentInternalKey === false) { + throw new Exception\RuntimeException("Iterator is on an invalid state"); + } + + // remove namespace prefix + return substr($this->currentInternalKey, $this->prefixLength); + } + + /** + * Move forward to next element + * + * @return void + * @throws Exception\RuntimeException + */ + public function next() + { + if ($this->currentInternalKey === false) { + throw new Exception\RuntimeException("Iterator is on an invalid state"); + } + + $this->currentInternalKey = dba_nextkey($this->handle); + + // Workaround for PHP-Bug #62492 + if ($this->currentInternalKey === null) { + $this->currentInternalKey = false; + } + } + + /** + * Checks if current position is valid + * + * @return bool + */ + public function valid() + { + return ($this->currentInternalKey !== false); + } + + /** + * Rewind the Iterator to the first element. + * + * @return void + * @throws Exception\RuntimeException + */ + public function rewind() + { + if ($this->currentInternalKey === false) { + throw new Exception\RuntimeException("Iterator is on an invalid state"); + } + + $this->currentInternalKey = dba_firstkey($this->handle); + + // Workaround for PHP-Bug #62492 + if ($this->currentInternalKey === null) { + $this->currentInternalKey = false; + } + } +} diff --git a/library/Zend/Cache/Storage/Adapter/DbaOptions.php b/library/Zend/Cache/Storage/Adapter/DbaOptions.php new file mode 100755 index 0000000000..13172b7498 --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/DbaOptions.php @@ -0,0 +1,129 @@ +triggerOptionEvent('namespace_separator', $namespaceSeparator); + $this->namespaceSeparator = $namespaceSeparator; + return $this; + } + + /** + * Get namespace separator + * + * @return string + */ + public function getNamespaceSeparator() + { + return $this->namespaceSeparator; + } + + /** + * Set pathname to database file + * + * @param string $pathname + * @return DbaOptions + */ + public function setPathname($pathname) + { + $this->pathname = (string) $pathname; + $this->triggerOptionEvent('pathname', $pathname); + return $this; + } + + /** + * Get pathname to database file + * + * @return string + */ + public function getPathname() + { + return $this->pathname; + } + + /** + * + * + * @param string $mode + * @return \Zend\Cache\Storage\Adapter\DbaOptions + */ + public function setMode($mode) + { + $this->mode = (string) $mode; + $this->triggerOptionEvent('mode', $mode); + return $this; + } + + public function getMode() + { + return $this->mode; + } + + public function setHandler($handler) + { + $handler = (string) $handler; + + if (!function_exists('dba_handlers') || !in_array($handler, dba_handlers())) { + throw new Exception\ExtensionNotLoadedException("DBA-Handler '{$handler}' not supported"); + } + + $this->triggerOptionEvent('handler', $handler); + $this->handler = $handler; + return $this; + } + + public function getHandler() + { + return $this->handler; + } +} diff --git a/library/Zend/Cache/Storage/Adapter/Filesystem.php b/library/Zend/Cache/Storage/Adapter/Filesystem.php new file mode 100755 index 0000000000..ef1c2a7dee --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/Filesystem.php @@ -0,0 +1,1616 @@ +options) { + $this->setOptions(new FilesystemOptions()); + } + return $this->options; + } + + /* FlushableInterface */ + + /** + * Flush the whole storage + * + * @throws Exception\RuntimeException + * @return bool + */ + public function flush() + { + $flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME; + $dir = $this->getOptions()->getCacheDir(); + $clearFolder = null; + $clearFolder = function ($dir) use (& $clearFolder, $flags) { + $it = new GlobIterator($dir . DIRECTORY_SEPARATOR . '*', $flags); + foreach ($it as $pathname) { + if ($it->isDir()) { + $clearFolder($pathname); + rmdir($pathname); + } else { + unlink($pathname); + } + } + }; + + ErrorHandler::start(); + $clearFolder($dir); + $error = ErrorHandler::stop(); + if ($error) { + throw new Exception\RuntimeException("Flushing directory '{$dir}' failed", 0, $error); + } + + return true; + } + + /* ClearExpiredInterface */ + + /** + * Remove expired items + * + * @return bool + * + * @triggers clearExpired.exception(ExceptionEvent) + */ + public function clearExpired() + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + + $flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_FILEINFO; + $path = $options->getCacheDir() + . str_repeat(DIRECTORY_SEPARATOR . $prefix . '*', $options->getDirLevel()) + . DIRECTORY_SEPARATOR . $prefix . '*.dat'; + $glob = new GlobIterator($path, $flags); + $time = time(); + $ttl = $options->getTtl(); + + ErrorHandler::start(); + foreach ($glob as $entry) { + $mtime = $entry->getMTime(); + if ($time >= $mtime + $ttl) { + $pathname = $entry->getPathname(); + unlink($pathname); + + $tagPathname = substr($pathname, 0, -4) . '.tag'; + if (file_exists($tagPathname)) { + unlink($tagPathname); + } + } + } + $error = ErrorHandler::stop(); + if ($error) { + $result = false; + return $this->triggerException( + __FUNCTION__, + new ArrayObject(), + $result, + new Exception\RuntimeException('Failed to clear expired items', 0, $error) + ); + } + + return true; + } + + /* ClearByNamespaceInterface */ + + /** + * Remove items by given namespace + * + * @param string $namespace + * @throws Exception\RuntimeException + * @return bool + */ + public function clearByNamespace($namespace) + { + $namespace = (string) $namespace; + if ($namespace === '') { + throw new Exception\InvalidArgumentException('No namespace given'); + } + + $options = $this->getOptions(); + $prefix = $namespace . $options->getNamespaceSeparator(); + + $flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME; + $path = $options->getCacheDir() + . str_repeat(DIRECTORY_SEPARATOR . $prefix . '*', $options->getDirLevel()) + . DIRECTORY_SEPARATOR . $prefix . '*.*'; + $glob = new GlobIterator($path, $flags); + + ErrorHandler::start(); + foreach ($glob as $pathname) { + unlink($pathname); + } + $error = ErrorHandler::stop(); + if ($error) { + throw new Exception\RuntimeException("Failed to remove files of '{$path}'", 0, $error); + } + + return true; + } + + /* ClearByPrefixInterface */ + + /** + * Remove items matching given prefix + * + * @param string $prefix + * @throws Exception\RuntimeException + * @return bool + */ + public function clearByPrefix($prefix) + { + $prefix = (string) $prefix; + if ($prefix === '') { + throw new Exception\InvalidArgumentException('No prefix given'); + } + + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $nsPrefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + + $flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME; + $path = $options->getCacheDir() + . str_repeat(DIRECTORY_SEPARATOR . $nsPrefix . '*', $options->getDirLevel()) + . DIRECTORY_SEPARATOR . $nsPrefix . $prefix . '*.*'; + $glob = new GlobIterator($path, $flags); + + ErrorHandler::start(); + foreach ($glob as $pathname) { + unlink($pathname); + } + $error = ErrorHandler::stop(); + if ($error) { + throw new Exception\RuntimeException("Failed to remove files of '{$path}'", 0, $error); + } + + return true; + } + + /* TaggableInterface */ + + /** + * Set tags to an item by given key. + * An empty array will remove all tags. + * + * @param string $key + * @param string[] $tags + * @return bool + */ + public function setTags($key, array $tags) + { + $this->normalizeKey($key); + if (!$this->internalHasItem($key)) { + return false; + } + + $filespec = $this->getFileSpec($key); + + if (!$tags) { + $this->unlink($filespec . '.tag'); + return true; + } + + $this->putFileContent($filespec . '.tag', implode("\n", $tags)); + return true; + } + + /** + * Get tags of an item by given key + * + * @param string $key + * @return string[]|FALSE + */ + public function getTags($key) + { + $this->normalizeKey($key); + if (!$this->internalHasItem($key)) { + return false; + } + + $filespec = $this->getFileSpec($key); + $tags = array(); + if (file_exists($filespec . '.tag')) { + $tags = explode("\n", $this->getFileContent($filespec . '.tag')); + } + + return $tags; + } + + /** + * Remove items matching given tags. + * + * If $disjunction only one of the given tags must match + * else all given tags must match. + * + * @param string[] $tags + * @param bool $disjunction + * @return bool + */ + public function clearByTags(array $tags, $disjunction = false) + { + if (!$tags) { + return true; + } + + $tagCount = count($tags); + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + + $flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME; + $path = $options->getCacheDir() + . str_repeat(DIRECTORY_SEPARATOR . $prefix . '*', $options->getDirLevel()) + . DIRECTORY_SEPARATOR . $prefix . '*.tag'; + $glob = new GlobIterator($path, $flags); + + foreach ($glob as $pathname) { + $diff = array_diff($tags, explode("\n", $this->getFileContent($pathname))); + + $rem = false; + if ($disjunction && count($diff) < $tagCount) { + $rem = true; + } elseif (!$disjunction && !$diff) { + $rem = true; + } + + if ($rem) { + unlink($pathname); + + $datPathname = substr($pathname, 0, -4) . '.dat'; + if (file_exists($datPathname)) { + unlink($datPathname); + } + } + } + + return true; + } + + /* IterableInterface */ + + /** + * Get the storage iterator + * + * @return FilesystemIterator + */ + public function getIterator() + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $path = $options->getCacheDir() + . str_repeat(DIRECTORY_SEPARATOR . $prefix . '*', $options->getDirLevel()) + . DIRECTORY_SEPARATOR . $prefix . '*.dat'; + return new FilesystemIterator($this, $path, $prefix); + } + + /* OptimizableInterface */ + + /** + * Optimize the storage + * + * @return bool + * @return Exception\RuntimeException + */ + public function optimize() + { + $options = $this->getOptions(); + if ($options->getDirLevel()) { + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + + // removes only empty directories + $this->rmDir($options->getCacheDir(), $prefix); + } + return true; + } + + /* TotalSpaceCapableInterface */ + + /** + * Get total space in bytes + * + * @throws Exception\RuntimeException + * @return int|float + */ + public function getTotalSpace() + { + if ($this->totalSpace === null) { + $path = $this->getOptions()->getCacheDir(); + + ErrorHandler::start(); + $total = disk_total_space($path); + $error = ErrorHandler::stop(); + if ($total === false) { + throw new Exception\RuntimeException("Can't detect total space of '{$path}'", 0, $error); + } + $this->totalSpace = $total; + + // clean total space buffer on change cache_dir + $events = $this->getEventManager(); + $handle = null; + $totalSpace = & $this->totalSpace; + $callback = function ($event) use (& $events, & $handle, & $totalSpace) { + $params = $event->getParams(); + if (isset($params['cache_dir'])) { + $totalSpace = null; + $events->detach($handle); + } + }; + $events->attach('option', $callback); + } + + return $this->totalSpace; + } + + /* AvailableSpaceCapableInterface */ + + /** + * Get available space in bytes + * + * @throws Exception\RuntimeException + * @return int|float + */ + public function getAvailableSpace() + { + $path = $this->getOptions()->getCacheDir(); + + ErrorHandler::start(); + $avail = disk_free_space($path); + $error = ErrorHandler::stop(); + if ($avail === false) { + throw new Exception\RuntimeException("Can't detect free space of '{$path}'", 0, $error); + } + + return $avail; + } + + /* reading */ + + /** + * Get an item. + * + * @param string $key + * @param bool $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + * + * @triggers getItem.pre(PreEvent) + * @triggers getItem.post(PostEvent) + * @triggers getItem.exception(ExceptionEvent) + */ + public function getItem($key, & $success = null, & $casToken = null) + { + $options = $this->getOptions(); + if ($options->getReadable() && $options->getClearStatCache()) { + clearstatcache(); + } + + $argn = func_num_args(); + if ($argn > 2) { + return parent::getItem($key, $success, $casToken); + } elseif ($argn > 1) { + return parent::getItem($key, $success); + } + + return parent::getItem($key); + } + + /** + * Get multiple items. + * + * @param array $keys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + * + * @triggers getItems.pre(PreEvent) + * @triggers getItems.post(PostEvent) + * @triggers getItems.exception(ExceptionEvent) + */ + public function getItems(array $keys) + { + $options = $this->getOptions(); + if ($options->getReadable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::getItems($keys); + } + + /** + * Internal method to get an item. + * + * @param string $normalizedKey + * @param bool $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) + { + if (!$this->internalHasItem($normalizedKey)) { + $success = false; + return null; + } + + try { + $filespec = $this->getFileSpec($normalizedKey); + $data = $this->getFileContent($filespec . '.dat'); + + // use filemtime + filesize as CAS token + if (func_num_args() > 2) { + $casToken = filemtime($filespec . '.dat') . filesize($filespec . '.dat'); + } + $success = true; + return $data; + } catch (BaseException $e) { + $success = false; + throw $e; + } + } + + /** + * Internal method to get multiple items. + * + * @param array $normalizedKeys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + */ + protected function internalGetItems(array & $normalizedKeys) + { + $keys = $normalizedKeys; // Don't change argument passed by reference + $result = array(); + while ($keys) { + // LOCK_NB if more than one items have to read + $nonBlocking = count($keys) > 1; + $wouldblock = null; + + // read items + foreach ($keys as $i => $key) { + if (!$this->internalHasItem($key)) { + unset($keys[$i]); + continue; + } + + $filespec = $this->getFileSpec($key); + $data = $this->getFileContent($filespec . '.dat', $nonBlocking, $wouldblock); + if ($nonBlocking && $wouldblock) { + continue; + } else { + unset($keys[$i]); + } + + $result[$key] = $data; + } + + // TODO: Don't check ttl after first iteration + // $options['ttl'] = 0; + } + + return $result; + } + + /** + * Test if an item exists. + * + * @param string $key + * @return bool + * @throws Exception\ExceptionInterface + * + * @triggers hasItem.pre(PreEvent) + * @triggers hasItem.post(PostEvent) + * @triggers hasItem.exception(ExceptionEvent) + */ + public function hasItem($key) + { + $options = $this->getOptions(); + if ($options->getReadable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::hasItem($key); + } + + /** + * Test multiple items. + * + * @param array $keys + * @return array Array of found keys + * @throws Exception\ExceptionInterface + * + * @triggers hasItems.pre(PreEvent) + * @triggers hasItems.post(PostEvent) + * @triggers hasItems.exception(ExceptionEvent) + */ + public function hasItems(array $keys) + { + $options = $this->getOptions(); + if ($options->getReadable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::hasItems($keys); + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalHasItem(& $normalizedKey) + { + $file = $this->getFileSpec($normalizedKey) . '.dat'; + if (!file_exists($file)) { + return false; + } + + $ttl = $this->getOptions()->getTtl(); + if ($ttl) { + ErrorHandler::start(); + $mtime = filemtime($file); + $error = ErrorHandler::stop(); + if (!$mtime) { + throw new Exception\RuntimeException("Error getting mtime of file '{$file}'", 0, $error); + } + + if (time() >= ($mtime + $ttl)) { + return false; + } + } + + return true; + } + + /** + * Get metadata + * + * @param string $key + * @return array|bool Metadata on success, false on failure + */ + public function getMetadata($key) + { + $options = $this->getOptions(); + if ($options->getReadable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::getMetadata($key); + } + + /** + * Get metadatas + * + * @param array $keys + * @param array $options + * @return array Associative array of keys and metadata + */ + public function getMetadatas(array $keys, array $options = array()) + { + $options = $this->getOptions(); + if ($options->getReadable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::getMetadatas($keys); + } + + /** + * Get info by key + * + * @param string $normalizedKey + * @return array|bool Metadata on success, false on failure + */ + protected function internalGetMetadata(& $normalizedKey) + { + if (!$this->internalHasItem($normalizedKey)) { + return false; + } + + $options = $this->getOptions(); + $filespec = $this->getFileSpec($normalizedKey); + $file = $filespec . '.dat'; + + $metadata = array( + 'filespec' => $filespec, + 'mtime' => filemtime($file) + ); + + if (!$options->getNoCtime()) { + $metadata['ctime'] = filectime($file); + } + + if (!$options->getNoAtime()) { + $metadata['atime'] = fileatime($file); + } + + return $metadata; + } + + /** + * Internal method to get multiple metadata + * + * @param array $normalizedKeys + * @return array Associative array of keys and metadata + * @throws Exception\ExceptionInterface + */ + protected function internalGetMetadatas(array & $normalizedKeys) + { + $options = $this->getOptions(); + $result = array(); + + foreach ($normalizedKeys as $normalizedKey) { + $filespec = $this->getFileSpec($normalizedKey); + $file = $filespec . '.dat'; + + $metadata = array( + 'filespec' => $filespec, + 'mtime' => filemtime($file), + ); + + if (!$options->getNoCtime()) { + $metadata['ctime'] = filectime($file); + } + + if (!$options->getNoAtime()) { + $metadata['atime'] = fileatime($file); + } + + $result[$normalizedKey] = $metadata; + } + + return $result; + } + + /* writing */ + + /** + * Store an item. + * + * @param string $key + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + * + * @triggers setItem.pre(PreEvent) + * @triggers setItem.post(PostEvent) + * @triggers setItem.exception(ExceptionEvent) + */ + public function setItem($key, $value) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + return parent::setItem($key, $value); + } + + /** + * Store multiple items. + * + * @param array $keyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + * + * @triggers setItems.pre(PreEvent) + * @triggers setItems.post(PostEvent) + * @triggers setItems.exception(ExceptionEvent) + */ + public function setItems(array $keyValuePairs) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::setItems($keyValuePairs); + } + + /** + * Add an item. + * + * @param string $key + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + * + * @triggers addItem.pre(PreEvent) + * @triggers addItem.post(PostEvent) + * @triggers addItem.exception(ExceptionEvent) + */ + public function addItem($key, $value) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::addItem($key, $value); + } + + /** + * Add multiple items. + * + * @param array $keyValuePairs + * @return bool + * @throws Exception\ExceptionInterface + * + * @triggers addItems.pre(PreEvent) + * @triggers addItems.post(PostEvent) + * @triggers addItems.exception(ExceptionEvent) + */ + public function addItems(array $keyValuePairs) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::addItems($keyValuePairs); + } + + /** + * Replace an existing item. + * + * @param string $key + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + * + * @triggers replaceItem.pre(PreEvent) + * @triggers replaceItem.post(PostEvent) + * @triggers replaceItem.exception(ExceptionEvent) + */ + public function replaceItem($key, $value) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::replaceItem($key, $value); + } + + /** + * Replace multiple existing items. + * + * @param array $keyValuePairs + * @return bool + * @throws Exception\ExceptionInterface + * + * @triggers replaceItems.pre(PreEvent) + * @triggers replaceItems.post(PostEvent) + * @triggers replaceItems.exception(ExceptionEvent) + */ + public function replaceItems(array $keyValuePairs) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::replaceItems($keyValuePairs); + } + + /** + * Internal method to store an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $filespec = $this->getFileSpec($normalizedKey); + $this->prepareDirectoryStructure($filespec); + + // write data in non-blocking mode + $wouldblock = null; + $this->putFileContent($filespec . '.dat', $value, true, $wouldblock); + + // delete related tag file (if present) + $this->unlink($filespec . '.tag'); + + // Retry writing data in blocking mode if it was blocked before + if ($wouldblock) { + $this->putFileContent($filespec . '.dat', $value); + } + + return true; + } + + /** + * Internal method to store multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalSetItems(array & $normalizedKeyValuePairs) + { + $oldUmask = null; + + // create an associated array of files and contents to write + $contents = array(); + foreach ($normalizedKeyValuePairs as $key => & $value) { + $filespec = $this->getFileSpec($key); + $this->prepareDirectoryStructure($filespec); + + // *.dat file + $contents[$filespec . '.dat'] = & $value; + + // *.tag file + $this->unlink($filespec . '.tag'); + } + + // write to disk + while ($contents) { + $nonBlocking = count($contents) > 1; + $wouldblock = null; + + foreach ($contents as $file => & $content) { + $this->putFileContent($file, $content, $nonBlocking, $wouldblock); + if (!$nonBlocking || !$wouldblock) { + unset($contents[$file]); + } + } + } + + // return OK + return array(); + } + + /** + * Set an item only if token matches + * + * It uses the token received from getItem() to check if the item has + * changed before overwriting it. + * + * @param mixed $token + * @param string $key + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + * @see getItem() + * @see setItem() + */ + public function checkAndSetItem($token, $key, $value) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::checkAndSetItem($token, $key, $value); + } + + /** + * Internal method to set an item only if token matches + * + * @param mixed $token + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + * @see getItem() + * @see setItem() + */ + protected function internalCheckAndSetItem(& $token, & $normalizedKey, & $value) + { + if (!$this->internalHasItem($normalizedKey)) { + return false; + } + + // use filemtime + filesize as CAS token + $file = $this->getFileSpec($normalizedKey) . '.dat'; + $check = filemtime($file) . filesize($file); + if ($token !== $check) { + return false; + } + + return $this->internalSetItem($normalizedKey, $value); + } + + /** + * Reset lifetime of an item + * + * @param string $key + * @return bool + * @throws Exception\ExceptionInterface + * + * @triggers touchItem.pre(PreEvent) + * @triggers touchItem.post(PostEvent) + * @triggers touchItem.exception(ExceptionEvent) + */ + public function touchItem($key) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::touchItem($key); + } + + /** + * Reset lifetime of multiple items. + * + * @param array $keys + * @return array Array of not updated keys + * @throws Exception\ExceptionInterface + * + * @triggers touchItems.pre(PreEvent) + * @triggers touchItems.post(PostEvent) + * @triggers touchItems.exception(ExceptionEvent) + */ + public function touchItems(array $keys) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::touchItems($keys); + } + + /** + * Internal method to reset lifetime of an item + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalTouchItem(& $normalizedKey) + { + if (!$this->internalHasItem($normalizedKey)) { + return false; + } + + $filespec = $this->getFileSpec($normalizedKey); + + ErrorHandler::start(); + $touch = touch($filespec . '.dat'); + $error = ErrorHandler::stop(); + if (!$touch) { + throw new Exception\RuntimeException("Error touching file '{$filespec}.dat'", 0, $error); + } + + return true; + } + + /** + * Remove an item. + * + * @param string $key + * @return bool + * @throws Exception\ExceptionInterface + * + * @triggers removeItem.pre(PreEvent) + * @triggers removeItem.post(PostEvent) + * @triggers removeItem.exception(ExceptionEvent) + */ + public function removeItem($key) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::removeItem($key); + } + + /** + * Remove multiple items. + * + * @param array $keys + * @return array Array of not removed keys + * @throws Exception\ExceptionInterface + * + * @triggers removeItems.pre(PreEvent) + * @triggers removeItems.post(PostEvent) + * @triggers removeItems.exception(ExceptionEvent) + */ + public function removeItems(array $keys) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::removeItems($keys); + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItem(& $normalizedKey) + { + $filespec = $this->getFileSpec($normalizedKey); + if (!file_exists($filespec . '.dat')) { + return false; + } else { + $this->unlink($filespec . '.dat'); + $this->unlink($filespec . '.tag'); + } + return true; + } + + /* status */ + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities === null) { + $marker = new stdClass(); + $options = $this->getOptions(); + + // detect metadata + $metadata = array('mtime', 'filespec'); + if (!$options->getNoAtime()) { + $metadata[] = 'atime'; + } + if (!$options->getNoCtime()) { + $metadata[] = 'ctime'; + } + + $capabilities = new Capabilities( + $this, + $marker, + array( + 'supportedDatatypes' => array( + 'NULL' => 'string', + 'boolean' => 'string', + 'integer' => 'string', + 'double' => 'string', + 'string' => true, + 'array' => false, + 'object' => false, + 'resource' => false, + ), + 'supportedMetadata' => $metadata, + 'minTtl' => 1, + 'maxTtl' => 0, + 'staticTtl' => false, + 'ttlPrecision' => 1, + 'expiredRead' => true, + 'maxKeyLength' => 251, // 255 - strlen(.dat | .tag) + 'namespaceIsPrefix' => true, + 'namespaceSeparator' => $options->getNamespaceSeparator(), + ) + ); + + // update capabilities on change options + $this->getEventManager()->attach('option', function ($event) use ($capabilities, $marker) { + $params = $event->getParams(); + + if (isset($params['namespace_separator'])) { + $capabilities->setNamespaceSeparator($marker, $params['namespace_separator']); + } + + if (isset($params['no_atime']) || isset($params['no_ctime'])) { + $metadata = $capabilities->getSupportedMetadata(); + + if (isset($params['no_atime']) && !$params['no_atime']) { + $metadata[] = 'atime'; + } elseif (isset($params['no_atime']) && ($index = array_search('atime', $metadata)) !== false) { + unset($metadata[$index]); + } + + if (isset($params['no_ctime']) && !$params['no_ctime']) { + $metadata[] = 'ctime'; + } elseif (isset($params['no_ctime']) && ($index = array_search('ctime', $metadata)) !== false) { + unset($metadata[$index]); + } + + $capabilities->setSupportedMetadata($marker, $metadata); + } + }); + + $this->capabilityMarker = $marker; + $this->capabilities = $capabilities; + } + + return $this->capabilities; + } + + /* internal */ + + /** + * Removes directories recursive by namespace + * + * @param string $dir Directory to delete + * @param string $prefix Namespace + Separator + * @return bool + */ + protected function rmDir($dir, $prefix) + { + $glob = glob( + $dir . DIRECTORY_SEPARATOR . $prefix . '*', + GLOB_ONLYDIR | GLOB_NOESCAPE | GLOB_NOSORT + ); + if (!$glob) { + // On some systems glob returns false even on empty result + return true; + } + + $ret = true; + foreach ($glob as $subdir) { + // skip removing current directory if removing of sub-directory failed + if ($this->rmDir($subdir, $prefix)) { + // ignore not empty directories + ErrorHandler::start(); + $ret = rmdir($subdir) && $ret; + ErrorHandler::stop(); + } else { + $ret = false; + } + } + + return $ret; + } + + /** + * Get file spec of the given key and namespace + * + * @param string $normalizedKey + * @return string + */ + protected function getFileSpec($normalizedKey) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $path = $options->getCacheDir() . DIRECTORY_SEPARATOR; + $level = $options->getDirLevel(); + + $fileSpecId = $path . $prefix . $normalizedKey . '/' . $level; + if ($this->lastFileSpecId !== $fileSpecId) { + if ($level > 0) { + // create up to 256 directories per directory level + $hash = md5($normalizedKey); + for ($i = 0, $max = ($level * 2); $i < $max; $i+= 2) { + $path .= $prefix . $hash[$i] . $hash[$i+1] . DIRECTORY_SEPARATOR; + } + } + + $this->lastFileSpecId = $fileSpecId; + $this->lastFileSpec = $path . $prefix . $normalizedKey; + } + + return $this->lastFileSpec; + } + + /** + * Read info file + * + * @param string $file + * @param bool $nonBlocking Don't block script if file is locked + * @param bool $wouldblock The optional argument is set to TRUE if the lock would block + * @return array|bool The info array or false if file wasn't found + * @throws Exception\RuntimeException + */ + protected function readInfoFile($file, $nonBlocking = false, & $wouldblock = null) + { + if (!file_exists($file)) { + return false; + } + + $content = $this->getFileContent($file, $nonBlocking, $wouldblock); + if ($nonBlocking && $wouldblock) { + return false; + } + + ErrorHandler::start(); + $ifo = unserialize($content); + $err = ErrorHandler::stop(); + if (!is_array($ifo)) { + throw new Exception\RuntimeException("Corrupted info file '{$file}'", 0, $err); + } + + return $ifo; + } + + /** + * Read a complete file + * + * @param string $file File complete path + * @param bool $nonBlocking Don't block script if file is locked + * @param bool $wouldblock The optional argument is set to TRUE if the lock would block + * @return string + * @throws Exception\RuntimeException + */ + protected function getFileContent($file, $nonBlocking = false, & $wouldblock = null) + { + $locking = $this->getOptions()->getFileLocking(); + $wouldblock = null; + + ErrorHandler::start(); + + // if file locking enabled -> file_get_contents can't be used + if ($locking) { + $fp = fopen($file, 'rb'); + if ($fp === false) { + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException("Error opening file '{$file}'", 0, $err); + } + + if ($nonBlocking) { + $lock = flock($fp, LOCK_SH | LOCK_NB, $wouldblock); + if ($wouldblock) { + fclose($fp); + ErrorHandler::stop(); + return; + } + } else { + $lock = flock($fp, LOCK_SH); + } + + if (!$lock) { + fclose($fp); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException("Error locking file '{$file}'", 0, $err); + } + + $res = stream_get_contents($fp); + if ($res === false) { + flock($fp, LOCK_UN); + fclose($fp); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException('Error getting stream contents', 0, $err); + } + + flock($fp, LOCK_UN); + fclose($fp); + + // if file locking disabled -> file_get_contents can be used + } else { + $res = file_get_contents($file, false); + if ($res === false) { + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException("Error getting file contents for file '{$file}'", 0, $err); + } + } + + ErrorHandler::stop(); + return $res; + } + + /** + * Prepares a directory structure for the given file(spec) + * using the configured directory level. + * + * @param string $file + * @return void + * @throws Exception\RuntimeException + */ + protected function prepareDirectoryStructure($file) + { + $options = $this->getOptions(); + $level = $options->getDirLevel(); + + // Directory structure is required only if directory level > 0 + if (!$level) { + return; + } + + // Directory structure already exists + $pathname = dirname($file); + if (file_exists($pathname)) { + return; + } + + $perm = $options->getDirPermission(); + $umask = $options->getUmask(); + if ($umask !== false && $perm !== false) { + $perm = $perm & ~$umask; + } + + ErrorHandler::start(); + + if ($perm === false || $level == 1) { + // build-in mkdir function is enough + + $umask = ($umask !== false) ? umask($umask) : false; + $res = mkdir($pathname, ($perm !== false) ? $perm : 0777, true); + + if ($umask !== false) { + umask($umask); + } + + if (!$res) { + $err = ErrorHandler::stop(); + + // Issue 6435: + // mkdir could fail because of a race condition it was already created by another process + // after the first file_exists above + if (file_exists($pathname)) { + return; + } + + $oct = ($perm === false) ? '777' : decoct($perm); + throw new Exception\RuntimeException("mkdir('{$pathname}', 0{$oct}, true) failed", 0, $err); + } + + if ($perm !== false && !chmod($pathname, $perm)) { + $oct = decoct($perm); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException("chmod('{$pathname}', 0{$oct}) failed", 0, $err); + } + } else { + // build-in mkdir function sets permission together with current umask + // which doesn't work well on multo threaded webservers + // -> create directories one by one and set permissions + + // find existing path and missing path parts + $parts = array(); + $path = $pathname; + while (!file_exists($path)) { + array_unshift($parts, basename($path)); + $nextPath = dirname($path); + if ($nextPath === $path) { + break; + } + $path = $nextPath; + } + + // make all missing path parts + foreach ($parts as $part) { + $path.= DIRECTORY_SEPARATOR . $part; + + // create a single directory, set and reset umask immediately + $umask = ($umask !== false) ? umask($umask) : false; + $res = mkdir($path, ($perm === false) ? 0777 : $perm, false); + if ($umask !== false) { + umask($umask); + } + + if (!$res) { + // Issue 6435: + // mkdir could fail because of a race condition it was already created by another process + // after the first file_exists above ... go to the next path part. + if (file_exists($path)) { + continue; + } + + $oct = ($perm === false) ? '777' : decoct($perm); + ErrorHandler::stop(); + throw new Exception\RuntimeException( + "mkdir('{$path}', 0{$oct}, false) failed" + ); + } + + if ($perm !== false && !chmod($path, $perm)) { + $oct = decoct($perm); + ErrorHandler::stop(); + throw new Exception\RuntimeException( + "chmod('{$path}', 0{$oct}) failed" + ); + } + } + } + + ErrorHandler::stop(); + } + + /** + * Write content to a file + * + * @param string $file File complete path + * @param string $data Data to write + * @param bool $nonBlocking Don't block script if file is locked + * @param bool $wouldblock The optional argument is set to TRUE if the lock would block + * @return void + * @throws Exception\RuntimeException + */ + protected function putFileContent($file, $data, $nonBlocking = false, & $wouldblock = null) + { + $options = $this->getOptions(); + $locking = $options->getFileLocking(); + $nonBlocking = $locking && $nonBlocking; + $wouldblock = null; + + $umask = $options->getUmask(); + $perm = $options->getFilePermission(); + if ($umask !== false && $perm !== false) { + $perm = $perm & ~$umask; + } + + ErrorHandler::start(); + + // if locking and non blocking is enabled -> file_put_contents can't used + if ($locking && $nonBlocking) { + $umask = ($umask !== false) ? umask($umask) : false; + + $fp = fopen($file, 'cb'); + + if ($umask) { + umask($umask); + } + + if (!$fp) { + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException("Error opening file '{$file}'", 0, $err); + } + + if ($perm !== false && !chmod($file, $perm)) { + fclose($fp); + $oct = decoct($perm); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException("chmod('{$file}', 0{$oct}) failed", 0, $err); + } + + if (!flock($fp, LOCK_EX | LOCK_NB, $wouldblock)) { + fclose($fp); + $err = ErrorHandler::stop(); + if ($wouldblock) { + return; + } else { + throw new Exception\RuntimeException("Error locking file '{$file}'", 0, $err); + } + } + + if (fwrite($fp, $data) === false) { + flock($fp, LOCK_UN); + fclose($fp); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException("Error writing file '{$file}'", 0, $err); + } + + if (!ftruncate($fp, strlen($data))) { + flock($fp, LOCK_UN); + fclose($fp); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException("Error truncating file '{$file}'", 0, $err); + } + + flock($fp, LOCK_UN); + fclose($fp); + + // else -> file_put_contents can be used + } else { + $flags = 0; + if ($locking) { + $flags = $flags | LOCK_EX; + } + + $umask = ($umask !== false) ? umask($umask) : false; + + $rs = file_put_contents($file, $data, $flags); + + if ($umask) { + umask($umask); + } + + if ($rs === false) { + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException("Error writing file '{$file}'", 0, $err); + } + + if ($perm !== false && !chmod($file, $perm)) { + $oct = decoct($perm); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException("chmod('{$file}', 0{$oct}) failed", 0, $err); + } + } + + ErrorHandler::stop(); + } + + /** + * Unlink a file + * + * @param string $file + * @return void + * @throws Exception\RuntimeException + */ + protected function unlink($file) + { + ErrorHandler::start(); + $res = unlink($file); + $err = ErrorHandler::stop(); + + // only throw exception if file still exists after deleting + if (!$res && file_exists($file)) { + throw new Exception\RuntimeException( + "Error unlinking file '{$file}'; file still exists", + 0, + $err + ); + } + } +} diff --git a/library/Zend/Cache/Storage/Adapter/FilesystemIterator.php b/library/Zend/Cache/Storage/Adapter/FilesystemIterator.php new file mode 100755 index 0000000000..447cfb3dbf --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/FilesystemIterator.php @@ -0,0 +1,179 @@ +storage = $storage; + $this->globIterator = new GlobIterator($path, GlobIterator::KEY_AS_FILENAME); + $this->prefix = $prefix; + $this->prefixLength = strlen($prefix); + } + + /** + * Get storage instance + * + * @return Filesystem + */ + public function getStorage() + { + return $this->storage; + } + + /** + * Get iterator mode + * + * @return int Value of IteratorInterface::CURRENT_AS_* + */ + public function getMode() + { + return $this->mode; + } + + /** + * Set iterator mode + * + * @param int $mode + * @return FilesystemIterator Fluent interface + */ + public function setMode($mode) + { + $this->mode = (int) $mode; + return $this; + } + + /* Iterator */ + + /** + * Get current key, value or metadata. + * + * @return mixed + */ + public function current() + { + if ($this->mode == IteratorInterface::CURRENT_AS_SELF) { + return $this; + } + + $key = $this->key(); + + if ($this->mode == IteratorInterface::CURRENT_AS_VALUE) { + return $this->storage->getItem($key); + } elseif ($this->mode == IteratorInterface::CURRENT_AS_METADATA) { + return $this->storage->getMetadata($key); + } + + return $key; + } + + /** + * Get current key + * + * @return string + */ + public function key() + { + $filename = $this->globIterator->key(); + + // return without namespace prefix and file suffix + return substr($filename, $this->prefixLength, -4); + } + + /** + * Move forward to next element + * + * @return void + */ + public function next() + { + $this->globIterator->next(); + } + + /** + * Checks if current position is valid + * + * @return bool + */ + public function valid() + { + try { + return $this->globIterator->valid(); + } catch (\LogicException $e) { + // @link https://bugs.php.net/bug.php?id=55701 + // GlobIterator throws LogicException with message + // 'The parent constructor was not called: the object is in an invalid state' + return false; + } + } + + /** + * Rewind the Iterator to the first element. + * + * @return bool false if the operation failed. + */ + public function rewind() + { + try { + return $this->globIterator->rewind(); + } catch (\LogicException $e) { + // @link https://bugs.php.net/bug.php?id=55701 + // GlobIterator throws LogicException with message + // 'The parent constructor was not called: the object is in an invalid state' + return false; + } + } +} diff --git a/library/Zend/Cache/Storage/Adapter/FilesystemOptions.php b/library/Zend/Cache/Storage/Adapter/FilesystemOptions.php new file mode 100755 index 0000000000..5eabbdf3f3 --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/FilesystemOptions.php @@ -0,0 +1,457 @@ +filePermission = false; + $this->dirPermission = false; + } + + parent::__construct($options); + } + + /** + * Set cache dir + * + * @param string $cacheDir + * @return FilesystemOptions + * @throws Exception\InvalidArgumentException + */ + public function setCacheDir($cacheDir) + { + if ($cacheDir !== null) { + if (!is_dir($cacheDir)) { + throw new Exception\InvalidArgumentException( + "Cache directory '{$cacheDir}' not found or not a directory" + ); + } elseif (!is_writable($cacheDir)) { + throw new Exception\InvalidArgumentException( + "Cache directory '{$cacheDir}' not writable" + ); + } elseif (!is_readable($cacheDir)) { + throw new Exception\InvalidArgumentException( + "Cache directory '{$cacheDir}' not readable" + ); + } + + $cacheDir = rtrim(realpath($cacheDir), DIRECTORY_SEPARATOR); + } else { + $cacheDir = sys_get_temp_dir(); + } + + $this->triggerOptionEvent('cache_dir', $cacheDir); + $this->cacheDir = $cacheDir; + return $this; + } + + /** + * Get cache dir + * + * @return null|string + */ + public function getCacheDir() + { + if ($this->cacheDir === null) { + $this->setCacheDir(null); + } + + return $this->cacheDir; + } + + /** + * Set clear stat cache + * + * @param bool $clearStatCache + * @return FilesystemOptions + */ + public function setClearStatCache($clearStatCache) + { + $clearStatCache = (bool) $clearStatCache; + $this->triggerOptionEvent('clear_stat_cache', $clearStatCache); + $this->clearStatCache = $clearStatCache; + return $this; + } + + /** + * Get clear stat cache + * + * @return bool + */ + public function getClearStatCache() + { + return $this->clearStatCache; + } + + /** + * Set dir level + * + * @param int $dirLevel + * @return FilesystemOptions + * @throws Exception\InvalidArgumentException + */ + public function setDirLevel($dirLevel) + { + $dirLevel = (int) $dirLevel; + if ($dirLevel < 0 || $dirLevel > 16) { + throw new Exception\InvalidArgumentException( + "Directory level '{$dirLevel}' must be between 0 and 16" + ); + } + $this->triggerOptionEvent('dir_level', $dirLevel); + $this->dirLevel = $dirLevel; + return $this; + } + + /** + * Get dir level + * + * @return int + */ + public function getDirLevel() + { + return $this->dirLevel; + } + + /** + * Set permission to create directories on unix systems + * + * @param false|string|int $dirPermission FALSE to disable explicit permission or an octal number + * @return FilesystemOptions + * @see setUmask + * @see setFilePermission + * @link http://php.net/manual/function.chmod.php + */ + public function setDirPermission($dirPermission) + { + if ($dirPermission !== false) { + if (is_string($dirPermission)) { + $dirPermission = octdec($dirPermission); + } else { + $dirPermission = (int) $dirPermission; + } + + // validate + if (($dirPermission & 0700) != 0700) { + throw new Exception\InvalidArgumentException( + 'Invalid directory permission: need permission to execute, read and write by owner' + ); + } + } + + if ($this->dirPermission !== $dirPermission) { + $this->triggerOptionEvent('dir_permission', $dirPermission); + $this->dirPermission = $dirPermission; + } + + return $this; + } + + /** + * Get permission to create directories on unix systems + * + * @return false|int + */ + public function getDirPermission() + { + return $this->dirPermission; + } + + /** + * Set file locking + * + * @param bool $fileLocking + * @return FilesystemOptions + */ + public function setFileLocking($fileLocking) + { + $fileLocking = (bool) $fileLocking; + $this->triggerOptionEvent('file_locking', $fileLocking); + $this->fileLocking = $fileLocking; + return $this; + } + + /** + * Get file locking + * + * @return bool + */ + public function getFileLocking() + { + return $this->fileLocking; + } + + /** + * Set permission to create files on unix systems + * + * @param false|string|int $filePermission FALSE to disable explicit permission or an octal number + * @return FilesystemOptions + * @see setUmask + * @see setDirPermission + * @link http://php.net/manual/function.chmod.php + */ + public function setFilePermission($filePermission) + { + if ($filePermission !== false) { + if (is_string($filePermission)) { + $filePermission = octdec($filePermission); + } else { + $filePermission = (int) $filePermission; + } + + // validate + if (($filePermission & 0600) != 0600) { + throw new Exception\InvalidArgumentException( + 'Invalid file permission: need permission to read and write by owner' + ); + } elseif ($filePermission & 0111) { + throw new Exception\InvalidArgumentException( + "Invalid file permission: Cache files shoudn't be executable" + ); + } + } + + if ($this->filePermission !== $filePermission) { + $this->triggerOptionEvent('file_permission', $filePermission); + $this->filePermission = $filePermission; + } + + return $this; + } + + /** + * Get permission to create files on unix systems + * + * @return false|int + */ + public function getFilePermission() + { + return $this->filePermission; + } + + /** + * Set namespace separator + * + * @param string $namespaceSeparator + * @return FilesystemOptions + */ + public function setNamespaceSeparator($namespaceSeparator) + { + $namespaceSeparator = (string) $namespaceSeparator; + $this->triggerOptionEvent('namespace_separator', $namespaceSeparator); + $this->namespaceSeparator = $namespaceSeparator; + return $this; + } + + /** + * Get namespace separator + * + * @return string + */ + public function getNamespaceSeparator() + { + return $this->namespaceSeparator; + } + + /** + * Set no atime + * + * @param bool $noAtime + * @return FilesystemOptions + */ + public function setNoAtime($noAtime) + { + $noAtime = (bool) $noAtime; + $this->triggerOptionEvent('no_atime', $noAtime); + $this->noAtime = $noAtime; + return $this; + } + + /** + * Get no atime + * + * @return bool + */ + public function getNoAtime() + { + return $this->noAtime; + } + + /** + * Set no ctime + * + * @param bool $noCtime + * @return FilesystemOptions + */ + public function setNoCtime($noCtime) + { + $noCtime = (bool) $noCtime; + $this->triggerOptionEvent('no_ctime', $noCtime); + $this->noCtime = $noCtime; + return $this; + } + + /** + * Get no ctime + * + * @return bool + */ + public function getNoCtime() + { + return $this->noCtime; + } + + /** + * Set the umask to create files and directories on unix systems + * + * Note: On multithreaded webservers it's better to explicit set file and dir permission. + * + * @param false|string|int $umask FALSE to disable umask or an octal number + * @return FilesystemOptions + * @see setFilePermission + * @see setDirPermission + * @link http://php.net/manual/function.umask.php + * @link http://en.wikipedia.org/wiki/Umask + */ + public function setUmask($umask) + { + if ($umask !== false) { + if (is_string($umask)) { + $umask = octdec($umask); + } else { + $umask = (int) $umask; + } + + // validate + if ($umask & 0700) { + throw new Exception\InvalidArgumentException( + 'Invalid umask: need permission to execute, read and write by owner' + ); + } + + // normalize + $umask = $umask & 0777; + } + + if ($this->umask !== $umask) { + $this->triggerOptionEvent('umask', $umask); + $this->umask = $umask; + } + + return $this; + } + + /** + * Get the umask to create files and directories on unix systems + * + * @return false|int + */ + public function getUmask() + { + return $this->umask; + } +} diff --git a/library/Zend/Cache/Storage/Adapter/KeyListIterator.php b/library/Zend/Cache/Storage/Adapter/KeyListIterator.php new file mode 100755 index 0000000000..86bcbe1609 --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/KeyListIterator.php @@ -0,0 +1,169 @@ +storage = $storage; + $this->keys = $keys; + $this->count = count($keys); + } + + /** + * Get storage instance + * + * @return StorageInterface + */ + public function getStorage() + { + return $this->storage; + } + + /** + * Get iterator mode + * + * @return int Value of IteratorInterface::CURRENT_AS_* + */ + public function getMode() + { + return $this->mode; + } + + /** + * Set iterator mode + * + * @param int $mode + * @return KeyListIterator Fluent interface + */ + public function setMode($mode) + { + $this->mode = (int) $mode; + return $this; + } + + /** + * Get current key, value or metadata. + * + * @return mixed + */ + public function current() + { + if ($this->mode == IteratorInterface::CURRENT_AS_SELF) { + return $this; + } + + $key = $this->key(); + + if ($this->mode == IteratorInterface::CURRENT_AS_METADATA) { + return $this->storage->getMetadata($key); + } elseif ($this->mode == IteratorInterface::CURRENT_AS_VALUE) { + return $this->storage->getItem($key); + } + + return $key; + } + + /** + * Get current key + * + * @return string + */ + public function key() + { + return $this->keys[$this->position]; + } + + /** + * Checks if current position is valid + * + * @return bool + */ + public function valid() + { + return $this->position < $this->count; + } + + /** + * Move forward to next element + * + * @return void + */ + public function next() + { + $this->position++; + } + + /** + * Rewind the Iterator to the first element. + * + * @return void + */ + public function rewind() + { + $this->position = 0; + } + + /** + * Count number of items + * + * @return int + */ + public function count() + { + return $this->count; + } +} diff --git a/library/Zend/Cache/Storage/Adapter/Memcache.php b/library/Zend/Cache/Storage/Adapter/Memcache.php new file mode 100755 index 0000000000..953e52cdac --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/Memcache.php @@ -0,0 +1,574 @@ + 0) { + throw new Exception\ExtensionNotLoadedException("Missing ext/memcache version >= 2.0.0"); + } + + parent::__construct($options); + + // reset initialized flag on update option(s) + $initialized = & $this->initialized; + $this->getEventManager()->attach('option', function ($event) use (& $initialized) { + $initialized = false; + }); + } + + /** + * Initialize the internal memcache resource + * + * @return MemcacheResource + */ + protected function getMemcacheResource() + { + if ($this->initialized) { + return $this->resourceManager->getResource($this->resourceId); + } + + $options = $this->getOptions(); + + // get resource manager and resource id + $this->resourceManager = $options->getResourceManager(); + $this->resourceId = $options->getResourceId(); + + // init namespace prefix + $this->namespacePrefix = ''; + $namespace = $options->getNamespace(); + if ($namespace !== '') { + $this->namespacePrefix = $namespace . $options->getNamespaceSeparator(); + } + + // update initialized flag + $this->initialized = true; + + return $this->resourceManager->getResource($this->resourceId); + } + + /* options */ + + /** + * Set options. + * + * @param array|Traversable|MemcacheOptions $options + * @return Memcache + * @see getOptions() + */ + public function setOptions($options) + { + if (!$options instanceof MemcacheOptions) { + $options = new MemcacheOptions($options); + } + + return parent::setOptions($options); + } + + /** + * Get options. + * + * @return MemcacheOptions + * @see setOptions() + */ + public function getOptions() + { + if (!$this->options) { + $this->setOptions(new MemcacheOptions()); + } + return $this->options; + } + + /** + * @param mixed $value + * @return int + */ + protected function getWriteFlag(& $value) + { + if (!$this->getOptions()->getCompression()) { + return 0; + } + // Don't compress numeric or boolean types + return (is_bool($value) || is_int($value) || is_float($value)) ? 0 : MEMCACHE_COMPRESSED; + } + + /* FlushableInterface */ + + /** + * Flush the whole storage + * + * @return bool + */ + public function flush() + { + $memc = $this->getMemcacheResource(); + if (!$memc->flush()) { + return new Exception\RuntimeException("Memcache flush failed"); + } + return true; + } + + /* TotalSpaceCapableInterface */ + + /** + * Get total space in bytes + * + * @return int|float + */ + public function getTotalSpace() + { + $memc = $this->getMemcacheResource(); + $stats = $memc->getExtendedStats(); + if ($stats === false) { + return new Exception\RuntimeException("Memcache getStats failed"); + } + + $mem = array_pop($stats); + return $mem['limit_maxbytes']; + } + + /* AvailableSpaceCapableInterface */ + + /** + * Get available space in bytes + * + * @return int|float + */ + public function getAvailableSpace() + { + $memc = $this->getMemcacheResource(); + $stats = $memc->getExtendedStats(); + if ($stats === false) { + throw new Exception\RuntimeException('Memcache getStats failed'); + } + + $mem = array_pop($stats); + return $mem['limit_maxbytes'] - $mem['bytes']; + } + + /* reading */ + + /** + * Internal method to get an item. + * + * @param string $normalizedKey + * @param bool $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) + { + $memc = $this->getMemcacheResource(); + $internalKey = $this->namespacePrefix . $normalizedKey; + + $result = $memc->get($internalKey); + $success = ($result !== false); + if ($result === false) { + return null; + } + + $casToken = $result; + return $result; + } + + /** + * Internal method to get multiple items. + * + * @param array $normalizedKeys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + */ + protected function internalGetItems(array & $normalizedKeys) + { + $memc = $this->getMemcacheResource(); + + foreach ($normalizedKeys as & $normalizedKey) { + $normalizedKey = $this->namespacePrefix . $normalizedKey; + } + + $result = $memc->get($normalizedKeys); + if ($result === false) { + return array(); + } + + // remove namespace prefix from result + if ($this->namespacePrefix !== '') { + $tmp = array(); + $nsPrefixLength = strlen($this->namespacePrefix); + foreach ($result as $internalKey => & $value) { + $tmp[substr($internalKey, $nsPrefixLength)] = & $value; + } + $result = $tmp; + } + + return $result; + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalHasItem(& $normalizedKey) + { + $memc = $this->getMemcacheResource(); + $value = $memc->get($this->namespacePrefix . $normalizedKey); + return ($value !== false); + } + + /** + * Internal method to test multiple items. + * + * @param array $normalizedKeys + * @return array Array of found keys + * @throws Exception\ExceptionInterface + */ + protected function internalHasItems(array & $normalizedKeys) + { + $memc = $this->getMemcacheResource(); + + foreach ($normalizedKeys as & $normalizedKey) { + $normalizedKey = $this->namespacePrefix . $normalizedKey; + } + + $result = $memc->get($normalizedKeys); + if ($result === false) { + return array(); + } + + // Convert to a single list + $result = array_keys($result); + + // remove namespace prefix + if ($result && $this->namespacePrefix !== '') { + $nsPrefixLength = strlen($this->namespacePrefix); + foreach ($result as & $internalKey) { + $internalKey = substr($internalKey, $nsPrefixLength); + } + } + + return $result; + } + + /** + * Get metadata of multiple items + * + * @param array $normalizedKeys + * @return array Associative array of keys and metadata + * @throws Exception\ExceptionInterface + */ + protected function internalGetMetadatas(array & $normalizedKeys) + { + $memc = $this->getMemcacheResource(); + + foreach ($normalizedKeys as & $normalizedKey) { + $normalizedKey = $this->namespacePrefix . $normalizedKey; + } + + $result = $memc->get($normalizedKeys); + if ($result === false) { + return array(); + } + + // remove namespace prefix and use an empty array as metadata + if ($this->namespacePrefix === '') { + foreach ($result as & $value) { + $value = array(); + } + return $result; + } + + $final = array(); + $nsPrefixLength = strlen($this->namespacePrefix); + foreach (array_keys($result) as $internalKey) { + $final[substr($internalKey, $nsPrefixLength)] = array(); + } + return $final; + } + + /* writing */ + + /** + * Internal method to store an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $memc = $this->getMemcacheResource(); + $expiration = $this->expirationTime(); + $flag = $this->getWriteFlag($value); + + if (!$memc->set($this->namespacePrefix . $normalizedKey, $value, $flag, $expiration)) { + throw new Exception\RuntimeException('Memcache set value failed'); + } + + return true; + } + + /** + * Add an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalAddItem(& $normalizedKey, & $value) + { + $memc = $this->getMemcacheResource(); + $expiration = $this->expirationTime(); + $flag = $this->getWriteFlag($value); + + return $memc->add($this->namespacePrefix . $normalizedKey, $value, $flag, $expiration); + } + + /** + * Internal method to replace an existing item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalReplaceItem(& $normalizedKey, & $value) + { + $memc = $this->getMemcacheResource(); + $expiration = $this->expirationTime(); + $flag = $this->getWriteFlag($value); + + return $memc->replace($this->namespacePrefix . $normalizedKey, $value, $flag, $expiration); + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItem(& $normalizedKey) + { + $memc = $this->getMemcacheResource(); + // Delete's second parameter (timeout) is deprecated and not supported. + // Values other than 0 may cause delete to fail. + // http://www.php.net/manual/memcache.delete.php + return $memc->delete($this->namespacePrefix . $normalizedKey, 0); + } + + /** + * Internal method to increment an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalIncrementItem(& $normalizedKey, & $value) + { + $memc = $this->getMemcacheResource(); + $internalKey = $this->namespacePrefix . $normalizedKey; + $value = (int) $value; + $newValue = $memc->increment($internalKey, $value); + + if ($newValue !== false) { + return $newValue; + } + + // Set initial value. Don't use compression! + // http://www.php.net/manual/memcache.increment.php + $newValue = $value; + if (!$memc->add($internalKey, $newValue, 0, $this->expirationTime())) { + throw new Exception\RuntimeException('Memcache unable to add increment value'); + } + + return $newValue; + } + + /** + * Internal method to decrement an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalDecrementItem(& $normalizedKey, & $value) + { + $memc = $this->getMemcacheResource(); + $internalKey = $this->namespacePrefix . $normalizedKey; + $value = (int) $value; + $newValue = $memc->decrement($internalKey, $value); + + if ($newValue !== false) { + return $newValue; + } + + // Set initial value. Don't use compression! + // http://www.php.net/manual/memcache.decrement.php + $newValue = -$value; + if (!$memc->add($internalKey, $newValue, 0, $this->expirationTime())) { + throw new Exception\RuntimeException('Memcache unable to add decrement value'); + } + + return $newValue; + } + + /* status */ + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities !== null) { + return $this->capabilities; + } + + if (version_compare('3.0.3', phpversion('memcache')) <= 0) { + // In ext/memcache v3.0.3: + // Scalar data types (int, bool, double) are preserved by get/set. + // http://pecl.php.net/package/memcache/3.0.3 + // + // This effectively removes support for `boolean` types since + // "not found" return values are === false. + $supportedDatatypes = array( + 'NULL' => true, + 'boolean' => false, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => true, + 'object' => 'object', + 'resource' => false, + ); + } else { + // In stable 2.x ext/memcache versions, scalar data types are + // converted to strings and must be manually cast back to original + // types by the user. + // + // ie. It is impossible to know if the saved value: (string)"1" + // was previously: (bool)true, (int)1, or (string)"1". + // Similarly, the saved value: (string)"" + // might have previously been: (bool)false or (string)"" + $supportedDatatypes = array( + 'NULL' => true, + 'boolean' => 'boolean', + 'integer' => 'integer', + 'double' => 'double', + 'string' => true, + 'array' => true, + 'object' => 'object', + 'resource' => false, + ); + } + + $this->capabilityMarker = new stdClass(); + $this->capabilities = new Capabilities( + $this, + $this->capabilityMarker, + array( + 'supportedDatatypes' => $supportedDatatypes, + 'supportedMetadata' => array(), + 'minTtl' => 1, + 'maxTtl' => 0, + 'staticTtl' => true, + 'ttlPrecision' => 1, + 'useRequestTime' => false, + 'expiredRead' => false, + 'maxKeyLength' => 255, + 'namespaceIsPrefix' => true, + ) + ); + + return $this->capabilities; + } + + /* internal */ + + /** + * Get expiration time by ttl + * + * Some storage commands involve sending an expiration value (relative to + * an item or to an operation requested by the client) to the server. In + * all such cases, the actual value sent may either be Unix time (number of + * seconds since January 1, 1970, as an integer), or a number of seconds + * starting from current time. In the latter case, this number of seconds + * may not exceed 60*60*24*30 (number of seconds in 30 days); if the + * expiration value is larger than that, the server will consider it to be + * real Unix time value rather than an offset from current time. + * + * @return int + */ + protected function expirationTime() + { + $ttl = $this->getOptions()->getTtl(); + if ($ttl > 2592000) { + return time() + $ttl; + } + return $ttl; + } +} diff --git a/library/Zend/Cache/Storage/Adapter/MemcacheOptions.php b/library/Zend/Cache/Storage/Adapter/MemcacheOptions.php new file mode 100755 index 0000000000..5fcdc3427b --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/MemcacheOptions.php @@ -0,0 +1,284 @@ +namespaceSeparator !== $namespaceSeparator) { + $this->triggerOptionEvent('namespace_separator', $namespaceSeparator); + $this->namespaceSeparator = $namespaceSeparator; + } + return $this; + } + + /** + * Get namespace separator + * + * @return string + */ + public function getNamespaceSeparator() + { + return $this->namespaceSeparator; + } + + /** + * Set the memcache resource manager to use + * + * @param null|MemcacheResourceManager $resourceManager + * @return MemcacheOptions + */ + public function setResourceManager(MemcacheResourceManager $resourceManager = null) + { + if ($this->resourceManager !== $resourceManager) { + $this->triggerOptionEvent('resource_manager', $resourceManager); + $this->resourceManager = $resourceManager; + } + return $this; + } + + /** + * Get the memcache resource manager + * + * @return MemcacheResourceManager + */ + public function getResourceManager() + { + if (!$this->resourceManager) { + $this->resourceManager = new MemcacheResourceManager(); + } + return $this->resourceManager; + } + + /** + * Get the memcache resource id + * + * @return string + */ + public function getResourceId() + { + return $this->resourceId; + } + + /** + * Set the memcache resource id + * + * @param string $resourceId + * @return MemcacheOptions + */ + public function setResourceId($resourceId) + { + $resourceId = (string) $resourceId; + if ($this->resourceId !== $resourceId) { + $this->triggerOptionEvent('resource_id', $resourceId); + $this->resourceId = $resourceId; + } + return $this; + } + + /** + * Is compressed writes turned on? + * + * @return boolean + */ + public function getCompression() + { + return $this->compression; + } + + /** + * Set whether compressed writes are turned on or not + * + * @param boolean $compression + * @return $this + */ + public function setCompression($compression) + { + $compression = (bool) $compression; + if ($this->compression !== $compression) { + $this->triggerOptionEvent('compression', $compression); + $this->compression = $compression; + } + return $this; + } + + /** + * Sets a list of memcache servers to add on initialize + * + * @param string|array $servers list of servers + * @return MemcacheOptions + * @throws Exception\InvalidArgumentException + */ + public function setServers($servers) + { + $this->getResourceManager()->addServers($this->getResourceId(), $servers); + return $this; + } + + /** + * Get Servers + * + * @return array + */ + public function getServers() + { + return $this->getResourceManager()->getServers($this->getResourceId()); + } + + /** + * Set compress threshold + * + * @param int|string|array|\ArrayAccess|null $threshold + * @return MemcacheOptions + */ + public function setAutoCompressThreshold($threshold) + { + $this->getResourceManager()->setAutoCompressThreshold($this->getResourceId(), $threshold); + return $this; + } + + /** + * Get compress threshold + * + * @return int|null + */ + public function getAutoCompressThreshold() + { + return $this->getResourceManager()->getAutoCompressThreshold($this->getResourceId()); + } + + /** + * Set compress min savings option + * + * @param float|string|null $minSavings + * @return MemcacheOptions + */ + public function setAutoCompressMinSavings($minSavings) + { + $this->getResourceManager()->setAutoCompressMinSavings($this->getResourceId(), $minSavings); + return $this; + } + + /** + * Get compress min savings + * + * @return Exception\RuntimeException + */ + public function getAutoCompressMinSavings() + { + return $this->getResourceManager()->getAutoCompressMinSavings($this->getResourceId()); + } + + /** + * Set default server values + * + * @param array $serverDefaults + * @return MemcacheOptions + */ + public function setServerDefaults(array $serverDefaults) + { + $this->getResourceManager()->setServerDefaults($this->getResourceId(), $serverDefaults); + return $this; + } + + /** + * Get default server values + * + * @return array + */ + public function getServerDefaults() + { + return $this->getResourceManager()->getServerDefaults($this->getResourceId()); + } + + /** + * Set callback for server connection failures + * + * @param callable $callback + * @return $this + */ + public function setFailureCallback($callback) + { + $this->getResourceManager()->setFailureCallback($this->getResourceId(), $callback); + return $this; + } + + /** + * Get callback for server connection failures + * + * @return callable + */ + public function getFailureCallback() + { + return $this->getResourceManager()->getFailureCallback($this->getResourceId()); + } +} diff --git a/library/Zend/Cache/Storage/Adapter/MemcacheResourceManager.php b/library/Zend/Cache/Storage/Adapter/MemcacheResourceManager.php new file mode 100755 index 0000000000..1fbff1828d --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/MemcacheResourceManager.php @@ -0,0 +1,646 @@ +resources[$id]); + } + + /** + * Gets a memcache resource + * + * @param string $id + * @return MemcacheResource + * @throws Exception\RuntimeException + */ + public function getResource($id) + { + if (!$this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + $resource = $this->resources[$id]; + if ($resource instanceof MemcacheResource) { + return $resource; + } + + $memc = new MemcacheResource(); + $this->setResourceAutoCompressThreshold( + $memc, + $resource['auto_compress_threshold'], + $resource['auto_compress_min_savings'] + ); + foreach ($resource['servers'] as $server) { + $this->addServerToResource( + $memc, + $server, + $this->serverDefaults[$id], + $this->failureCallbacks[$id] + ); + } + + // buffer and return + $this->resources[$id] = $memc; + return $memc; + } + + /** + * Set a resource + * + * @param string $id + * @param array|Traversable|MemcacheResource $resource + * @return MemcacheResourceManager + */ + public function setResource($id, $resource, $failureCallback = null, $serverDefaults = array()) + { + $id = (string) $id; + + if ($serverDefaults instanceof Traversable) { + $serverDefaults = ArrayUtils::iteratorToArray($serverDefaults); + } elseif (!is_array($serverDefaults)) { + throw new Exception\InvalidArgumentException( + 'ServerDefaults must be an instance Traversable or an array' + ); + } + + if (!($resource instanceof MemcacheResource)) { + if ($resource instanceof Traversable) { + $resource = ArrayUtils::iteratorToArray($resource); + } elseif (!is_array($resource)) { + throw new Exception\InvalidArgumentException( + 'Resource must be an instance of Memcache or an array or Traversable' + ); + } + + if (isset($resource['server_defaults'])) { + $serverDefaults = array_merge($serverDefaults, $resource['server_defaults']); + unset($resource['server_defaults']); + } + + $resourceOptions = array( + 'servers' => array(), + 'auto_compress_threshold' => null, + 'auto_compress_min_savings' => null, + ); + $resource = array_merge($resourceOptions, $resource); + + // normalize and validate params + $this->normalizeAutoCompressThreshold( + $resource['auto_compress_threshold'], + $resource['auto_compress_min_savings'] + ); + $this->normalizeServers($resource['servers']); + } + + $this->normalizeServerDefaults($serverDefaults); + + $this->resources[$id] = $resource; + $this->failureCallbacks[$id] = $failureCallback; + $this->serverDefaults[$id] = $serverDefaults; + + return $this; + } + + /** + * Remove a resource + * + * @param string $id + * @return MemcacheResourceManager + */ + public function removeResource($id) + { + unset($this->resources[$id]); + return $this; + } + + /** + * Normalize compress threshold options + * + * @param int|string|array|ArrayAccess $threshold + * @param float|string $minSavings + */ + protected function normalizeAutoCompressThreshold(& $threshold, & $minSavings) + { + if (is_array($threshold) || ($threshold instanceof ArrayAccess)) { + $tmpThreshold = (isset($threshold['threshold'])) ? $threshold['threshold'] : null; + $minSavings = (isset($threshold['min_savings'])) ? $threshold['min_savings'] : $minSavings; + $threshold = $tmpThreshold; + } + if (isset($threshold)) { + $threshold = (int) $threshold; + } + if (isset($minSavings)) { + $minSavings = (float) $minSavings; + } + } + + /** + * Set compress threshold on a Memcache resource + * + * @param MemcacheResource $resource + * @param array $libOptions + */ + protected function setResourceAutoCompressThreshold(MemcacheResource $resource, $threshold, $minSavings) + { + if (!isset($threshold)) { + return; + } + if (isset($minSavings)) { + $resource->setCompressThreshold($threshold, $minSavings); + } else { + $resource->setCompressThreshold($threshold); + } + } + + /** + * Get compress threshold + * + * @param string $id + * @return int|null + * @throws \Zend\Cache\Exception\RuntimeException + */ + public function getAutoCompressThreshold($id) + { + if (!$this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + $resource = & $this->resources[$id]; + if ($resource instanceof MemcacheResource) { + // Cannot get options from Memcache resource once created + throw new Exception\RuntimeException("Cannot get compress threshold once resource is created"); + } + return $resource['auto_compress_threshold']; + } + + /** + * Set compress threshold + * + * @param string $id + * @param int|string|array|ArrayAccess|null $threshold + * @param float|string|bool $minSavings + * @return MemcacheResourceManager + */ + public function setAutoCompressThreshold($id, $threshold, $minSavings = false) + { + if (!$this->hasResource($id)) { + return $this->setResource($id, array( + 'auto_compress_threshold' => $threshold, + )); + } + + $this->normalizeAutoCompressThreshold($threshold, $minSavings); + + $resource = & $this->resources[$id]; + if ($resource instanceof MemcacheResource) { + $this->setResourceAutoCompressThreshold($resource, $threshold, $minSavings); + } else { + $resource['auto_compress_threshold'] = $threshold; + if ($minSavings !== false) { + $resource['auto_compress_min_savings'] = $minSavings; + } + } + return $this; + } + + /** + * Get compress min savings + * + * @param string $id + * @return float|null + * @throws Exception\RuntimeException + */ + public function getAutoCompressMinSavings($id) + { + if (!$this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + $resource = & $this->resources[$id]; + if ($resource instanceof MemcacheResource) { + // Cannot get options from Memcache resource once created + throw new Exception\RuntimeException("Cannot get compress min savings once resource is created"); + } + return $resource['auto_compress_min_savings']; + } + + /** + * Set compress min savings + * + * @param string $id + * @param float|string|null $minSavings + * @return MemcacheResourceManager + * @throws \Zend\Cache\Exception\RuntimeException + */ + public function setAutoCompressMinSavings($id, $minSavings) + { + if (!$this->hasResource($id)) { + return $this->setResource($id, array( + 'auto_compress_min_savings' => $minSavings, + )); + } + + $minSavings = (float) $minSavings; + + $resource = & $this->resources[$id]; + if ($resource instanceof MemcacheResource) { + throw new Exception\RuntimeException( + "Cannot set compress min savings without a threshold value once a resource is created" + ); + } else { + $resource['auto_compress_min_savings'] = $minSavings; + } + return $this; + } + + /** + * Set default server values + * array( + * 'persistent' => , 'weight' => , + * 'timeout' => , 'retry_interval' => , + * ) + * @param string $id + * @param array $serverDefaults + * @return MemcacheResourceManager + */ + public function setServerDefaults($id, array $serverDefaults) + { + if (!$this->hasResource($id)) { + return $this->setResource($id, array( + 'server_defaults' => $serverDefaults + )); + } + + $this->normalizeServerDefaults($serverDefaults); + $this->serverDefaults[$id] = $serverDefaults; + + return $this; + } + + /** + * Get default server values + * + * @param string $id + * @return array + * @throws Exception\RuntimeException + */ + public function getServerDefaults($id) + { + if (!isset($this->serverDefaults[$id])) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + return $this->serverDefaults[$id]; + } + + /** + * @param array $serverDefaults + * @throws Exception\InvalidArgumentException + */ + protected function normalizeServerDefaults(& $serverDefaults) + { + if (!is_array($serverDefaults) && !($serverDefaults instanceof Traversable)) { + throw new Exception\InvalidArgumentException( + "Server defaults must be an array or an instance of Traversable" + ); + } + + // Defaults + $result = array( + 'persistent' => true, + 'weight' => 1, + 'timeout' => 1, // seconds + 'retry_interval' => 15, // seconds + ); + + foreach ($serverDefaults as $key => $value) { + switch ($key) { + case 'persistent': + $value = (bool) $value; + break; + case 'weight': + case 'timeout': + case 'retry_interval': + $value = (int) $value; + break; + } + $result[$key] = $value; + } + + $serverDefaults = $result; + } + + /** + * Set callback for server connection failures + * + * @param string $id + * @param callable|null $failureCallback + * @return MemcacheResourceManager + */ + public function setFailureCallback($id, $failureCallback) + { + if (!$this->hasResource($id)) { + return $this->setResource($id, array(), $failureCallback); + } + + $this->failureCallbacks[$id] = $failureCallback; + return $this; + } + + /** + * Get callback for server connection failures + * + * @param string $id + * @return callable|null + * @throws Exception\RuntimeException + */ + public function getFailureCallback($id) + { + if (!isset($this->failureCallbacks[$id])) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + return $this->failureCallbacks[$id]; + } + + /** + * Get servers + * + * @param string $id + * @throws Exception\RuntimeException + * @return array array('host' => , 'port' => , 'weight' => ) + */ + public function getServers($id) + { + if (!$this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + $resource = & $this->resources[$id]; + if ($resource instanceof MemcacheResource) { + throw new Exception\RuntimeException("Cannot get server list once resource is created"); + } + return $resource['servers']; + } + + /** + * Add servers + * + * @param string $id + * @param string|array $servers + * @return MemcacheResourceManager + */ + public function addServers($id, $servers) + { + if (!$this->hasResource($id)) { + return $this->setResource($id, array( + 'servers' => $servers + )); + } + + $this->normalizeServers($servers); + + $resource = & $this->resources[$id]; + if ($resource instanceof MemcacheResource) { + foreach ($servers as $server) { + $this->addServerToResource( + $resource, + $server, + $this->serverDefaults[$id], + $this->failureCallbacks[$id] + ); + } + } else { + // don't add servers twice + $resource['servers'] = array_merge( + $resource['servers'], + array_udiff($servers, $resource['servers'], array($this, 'compareServers')) + ); + } + + return $this; + } + + /** + * Add one server + * + * @param string $id + * @param string|array $server + * @return MemcacheResourceManager + */ + public function addServer($id, $server) + { + return $this->addServers($id, array($server)); + } + + /** + * @param MemcacheResource $resource + * @param array $server + * @param array $serverDefaults + * @param callable|null $failureCallback + */ + protected function addServerToResource( + MemcacheResource $resource, + array $server, + array $serverDefaults, + $failureCallback + ) { + // Apply server defaults + $server = array_merge($serverDefaults, $server); + + // Reorder parameters + $params = array( + $server['host'], + $server['port'], + $server['persistent'], + $server['weight'], + $server['timeout'], + $server['retry_interval'], + $server['status'], + ); + if (isset($failureCallback)) { + $params[] = $failureCallback; + } + call_user_func_array(array($resource, 'addServer'), $params); + } + + /** + * Normalize a list of servers into the following format: + * array(array('host' => , 'port' => , 'weight' => )[, ...]) + * + * @param string|array $servers + */ + protected function normalizeServers(& $servers) + { + if (is_string($servers)) { + // Convert string into a list of servers + $servers = explode(',', $servers); + } + + $result = array(); + foreach ($servers as $server) { + $this->normalizeServer($server); + $result[$server['host'] . ':' . $server['port']] = $server; + } + + $servers = array_values($result); + } + + /** + * Normalize one server into the following format: + * array( + * 'host' => , 'port' => , 'weight' => , + * 'status' => , 'persistent' => , + * 'timeout' => , 'retry_interval' => , + * ) + * + * @param string|array $server + * @throws Exception\InvalidArgumentException + */ + protected function normalizeServer(& $server) + { + // WARNING: The order of this array is important. + // Used for converting an ordered array to a keyed array. + // Append new options, do not insert or you will break BC. + $sTmp = array( + 'host' => null, + 'port' => 11211, + 'weight' => null, + 'status' => true, + 'persistent' => null, + 'timeout' => null, + 'retry_interval' => null, + ); + + // convert a single server into an array + if ($server instanceof Traversable) { + $server = ArrayUtils::iteratorToArray($server); + } + + if (is_array($server)) { + if (isset($server[0])) { + // Convert ordered array to keyed array + // array([, [, [, [, [, [, ]]]]]]) + $server = array_combine( + array_slice(array_keys($sTmp), 0, count($server)), + $server + ); + } + $sTmp = array_merge($sTmp, $server); + } elseif (is_string($server)) { + // parse server from URI host{:?port}{?weight} + $server = trim($server); + if (strpos($server, '://') === false) { + $server = 'tcp://' . $server; + } + + $urlParts = parse_url($server); + if (!$urlParts) { + throw new Exception\InvalidArgumentException("Invalid server given"); + } + + $sTmp = array_merge($sTmp, array_intersect_key($urlParts, $sTmp)); + if (isset($urlParts['query'])) { + $query = null; + parse_str($urlParts['query'], $query); + $sTmp = array_merge($sTmp, array_intersect_key($query, $sTmp)); + } + } + + if (!$sTmp['host']) { + throw new Exception\InvalidArgumentException('Missing required server host'); + } + + // Filter values + foreach ($sTmp as $key => $value) { + if (isset($value)) { + switch ($key) { + case 'host': + $value = (string) $value; + break; + case 'status': + case 'persistent': + $value = (bool) $value; + break; + case 'port': + case 'weight': + case 'timeout': + case 'retry_interval': + $value = (int) $value; + break; + } + } + $sTmp[$key] = $value; + } + $sTmp = array_filter( + $sTmp, + function ($val) { + return isset($val); + } + ); + + $server = $sTmp; + } + + /** + * Compare 2 normalized server arrays + * (Compares only the host and the port) + * + * @param array $serverA + * @param array $serverB + * @return int + */ + protected function compareServers(array $serverA, array $serverB) + { + $keyA = $serverA['host'] . ':' . $serverA['port']; + $keyB = $serverB['host'] . ':' . $serverB['port']; + if ($keyA === $keyB) { + return 0; + } + return $keyA > $keyB ? 1 : -1; + } +} diff --git a/library/Zend/Cache/Storage/Adapter/Memcached.php b/library/Zend/Cache/Storage/Adapter/Memcached.php new file mode 100755 index 0000000000..54bbed7832 --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/Memcached.php @@ -0,0 +1,703 @@ += 1.0.0'); + } + + parent::__construct($options); + + // reset initialized flag on update option(s) + $initialized = & $this->initialized; + $this->getEventManager()->attach('option', function ($event) use (& $initialized) { + $initialized = false; + }); + } + + /** + * Initialize the internal memcached resource + * + * @return MemcachedResource + */ + protected function getMemcachedResource() + { + if (!$this->initialized) { + $options = $this->getOptions(); + + // get resource manager and resource id + $this->resourceManager = $options->getResourceManager(); + $this->resourceId = $options->getResourceId(); + + // init namespace prefix + $namespace = $options->getNamespace(); + if ($namespace !== '') { + $this->namespacePrefix = $namespace . $options->getNamespaceSeparator(); + } else { + $this->namespacePrefix = ''; + } + + // update initialized flag + $this->initialized = true; + } + + return $this->resourceManager->getResource($this->resourceId); + } + + /* options */ + + /** + * Set options. + * + * @param array|Traversable|MemcachedOptions $options + * @return Memcached + * @see getOptions() + */ + public function setOptions($options) + { + if (!$options instanceof MemcachedOptions) { + $options = new MemcachedOptions($options); + } + + return parent::setOptions($options); + } + + /** + * Get options. + * + * @return MemcachedOptions + * @see setOptions() + */ + public function getOptions() + { + if (!$this->options) { + $this->setOptions(new MemcachedOptions()); + } + return $this->options; + } + + /* FlushableInterface */ + + /** + * Flush the whole storage + * + * @return bool + */ + public function flush() + { + $memc = $this->getMemcachedResource(); + if (!$memc->flush()) { + throw $this->getExceptionByResultCode($memc->getResultCode()); + } + return true; + } + + /* TotalSpaceCapableInterface */ + + /** + * Get total space in bytes + * + * @return int|float + */ + public function getTotalSpace() + { + $memc = $this->getMemcachedResource(); + $stats = $memc->getStats(); + if ($stats === false) { + throw new Exception\RuntimeException($memc->getResultMessage()); + } + + $mem = array_pop($stats); + return $mem['limit_maxbytes']; + } + + /* AvailableSpaceCapableInterface */ + + /** + * Get available space in bytes + * + * @return int|float + */ + public function getAvailableSpace() + { + $memc = $this->getMemcachedResource(); + $stats = $memc->getStats(); + if ($stats === false) { + throw new Exception\RuntimeException($memc->getResultMessage()); + } + + $mem = array_pop($stats); + return $mem['limit_maxbytes'] - $mem['bytes']; + } + + /* reading */ + + /** + * Internal method to get an item. + * + * @param string $normalizedKey + * @param bool $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) + { + $memc = $this->getMemcachedResource(); + $internalKey = $this->namespacePrefix . $normalizedKey; + + if (func_num_args() > 2) { + $result = $memc->get($internalKey, null, $casToken); + } else { + $result = $memc->get($internalKey); + } + + $success = true; + if ($result === false) { + $rsCode = $memc->getResultCode(); + if ($rsCode == MemcachedResource::RES_NOTFOUND) { + $result = null; + $success = false; + } elseif ($rsCode) { + $success = false; + throw $this->getExceptionByResultCode($rsCode); + } + } + + return $result; + } + + /** + * Internal method to get multiple items. + * + * @param array $normalizedKeys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + */ + protected function internalGetItems(array & $normalizedKeys) + { + $memc = $this->getMemcachedResource(); + + foreach ($normalizedKeys as & $normalizedKey) { + $normalizedKey = $this->namespacePrefix . $normalizedKey; + } + + $result = $memc->getMulti($normalizedKeys); + if ($result === false) { + throw $this->getExceptionByResultCode($memc->getResultCode()); + } + + // remove namespace prefix from result + if ($result && $this->namespacePrefix !== '') { + $tmp = array(); + $nsPrefixLength = strlen($this->namespacePrefix); + foreach ($result as $internalKey => & $value) { + $tmp[substr($internalKey, $nsPrefixLength)] = & $value; + } + $result = $tmp; + } + + return $result; + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalHasItem(& $normalizedKey) + { + $memc = $this->getMemcachedResource(); + $value = $memc->get($this->namespacePrefix . $normalizedKey); + if ($value === false) { + $rsCode = $memc->getResultCode(); + if ($rsCode == MemcachedResource::RES_SUCCESS) { + return true; + } elseif ($rsCode == MemcachedResource::RES_NOTFOUND) { + return false; + } else { + throw $this->getExceptionByResultCode($rsCode); + } + } + + return true; + } + + /** + * Internal method to test multiple items. + * + * @param array $normalizedKeys + * @return array Array of found keys + * @throws Exception\ExceptionInterface + */ + protected function internalHasItems(array & $normalizedKeys) + { + $memc = $this->getMemcachedResource(); + + foreach ($normalizedKeys as & $normalizedKey) { + $normalizedKey = $this->namespacePrefix . $normalizedKey; + } + + $result = $memc->getMulti($normalizedKeys); + if ($result === false) { + throw $this->getExceptionByResultCode($memc->getResultCode()); + } + + // Convert to a simgle list + $result = array_keys($result); + + // remove namespace prefix + if ($result && $this->namespacePrefix !== '') { + $nsPrefixLength = strlen($this->namespacePrefix); + foreach ($result as & $internalKey) { + $internalKey = substr($internalKey, $nsPrefixLength); + } + } + + return $result; + } + + /** + * Get metadata of multiple items + * + * @param array $normalizedKeys + * @return array Associative array of keys and metadata + * @throws Exception\ExceptionInterface + */ + protected function internalGetMetadatas(array & $normalizedKeys) + { + $memc = $this->getMemcachedResource(); + + foreach ($normalizedKeys as & $normalizedKey) { + $normalizedKey = $this->namespacePrefix . $normalizedKey; + } + + $result = $memc->getMulti($normalizedKeys); + if ($result === false) { + throw $this->getExceptionByResultCode($memc->getResultCode()); + } + + // remove namespace prefix and use an empty array as metadata + if ($this->namespacePrefix !== '') { + $tmp = array(); + $nsPrefixLength = strlen($this->namespacePrefix); + foreach (array_keys($result) as $internalKey) { + $tmp[substr($internalKey, $nsPrefixLength)] = array(); + } + $result = $tmp; + } else { + foreach ($result as & $value) { + $value = array(); + } + } + + return $result; + } + + /* writing */ + + /** + * Internal method to store an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $memc = $this->getMemcachedResource(); + $expiration = $this->expirationTime(); + if (!$memc->set($this->namespacePrefix . $normalizedKey, $value, $expiration)) { + throw $this->getExceptionByResultCode($memc->getResultCode()); + } + + return true; + } + + /** + * Internal method to store multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalSetItems(array & $normalizedKeyValuePairs) + { + $memc = $this->getMemcachedResource(); + $expiration = $this->expirationTime(); + + $namespacedKeyValuePairs = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => & $value) { + $namespacedKeyValuePairs[$this->namespacePrefix . $normalizedKey] = & $value; + } + + if (!$memc->setMulti($namespacedKeyValuePairs, $expiration)) { + throw $this->getExceptionByResultCode($memc->getResultCode()); + } + + return array(); + } + + /** + * Add an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalAddItem(& $normalizedKey, & $value) + { + $memc = $this->getMemcachedResource(); + $expiration = $this->expirationTime(); + if (!$memc->add($this->namespacePrefix . $normalizedKey, $value, $expiration)) { + if ($memc->getResultCode() == MemcachedResource::RES_NOTSTORED) { + return false; + } + throw $this->getExceptionByResultCode($memc->getResultCode()); + } + + return true; + } + + /** + * Internal method to replace an existing item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalReplaceItem(& $normalizedKey, & $value) + { + $memc = $this->getMemcachedResource(); + $expiration = $this->expirationTime(); + if (!$memc->replace($this->namespacePrefix . $normalizedKey, $value, $expiration)) { + $rsCode = $memc->getResultCode(); + if ($rsCode == MemcachedResource::RES_NOTSTORED) { + return false; + } + throw $this->getExceptionByResultCode($rsCode); + } + + return true; + } + + /** + * Internal method to set an item only if token matches + * + * @param mixed $token + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + * @see getItem() + * @see setItem() + */ + protected function internalCheckAndSetItem(& $token, & $normalizedKey, & $value) + { + $memc = $this->getMemcachedResource(); + $expiration = $this->expirationTime(); + $result = $memc->cas($token, $this->namespacePrefix . $normalizedKey, $value, $expiration); + + if ($result === false) { + $rsCode = $memc->getResultCode(); + if ($rsCode !== 0 && $rsCode != MemcachedResource::RES_DATA_EXISTS) { + throw $this->getExceptionByResultCode($rsCode); + } + } + + return $result; + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItem(& $normalizedKey) + { + $memc = $this->getMemcachedResource(); + $result = $memc->delete($this->namespacePrefix . $normalizedKey); + + if ($result === false) { + $rsCode = $memc->getResultCode(); + if ($rsCode == MemcachedResource::RES_NOTFOUND) { + return false; + } elseif ($rsCode != MemcachedResource::RES_SUCCESS) { + throw $this->getExceptionByResultCode($rsCode); + } + } + + return true; + } + + /** + * Internal method to remove multiple items. + * + * @param array $normalizedKeys + * @return array Array of not removed keys + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItems(array & $normalizedKeys) + { + // support for removing multiple items at once has been added in ext/memcached-2.0.0 + if (static::$extMemcachedMajorVersion < 2) { + return parent::internalRemoveItems($normalizedKeys); + } + + $memc = $this->getMemcachedResource(); + + foreach ($normalizedKeys as & $normalizedKey) { + $normalizedKey = $this->namespacePrefix . $normalizedKey; + } + + $rsCodes = $memc->deleteMulti($normalizedKeys); + + $missingKeys = array(); + foreach ($rsCodes as $key => $rsCode) { + if ($rsCode !== true && $rsCode != MemcachedResource::RES_SUCCESS) { + if ($rsCode != MemcachedResource::RES_NOTFOUND) { + throw $this->getExceptionByResultCode($rsCode); + } + $missingKeys[] = $key; + } + } + + // remove namespace prefix + if ($missingKeys && $this->namespacePrefix !== '') { + $nsPrefixLength = strlen($this->namespacePrefix); + foreach ($missingKeys as & $missingKey) { + $missingKey = substr($missingKey, $nsPrefixLength); + } + } + + return $missingKeys; + } + + /** + * Internal method to increment an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalIncrementItem(& $normalizedKey, & $value) + { + $memc = $this->getMemcachedResource(); + $internalKey = $this->namespacePrefix . $normalizedKey; + $value = (int) $value; + $newValue = $memc->increment($internalKey, $value); + + if ($newValue === false) { + $rsCode = $memc->getResultCode(); + + // initial value + if ($rsCode == MemcachedResource::RES_NOTFOUND) { + $newValue = $value; + $memc->add($internalKey, $newValue, $this->expirationTime()); + $rsCode = $memc->getResultCode(); + } + + if ($rsCode) { + throw $this->getExceptionByResultCode($rsCode); + } + } + + return $newValue; + } + + /** + * Internal method to decrement an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalDecrementItem(& $normalizedKey, & $value) + { + $memc = $this->getMemcachedResource(); + $internalKey = $this->namespacePrefix . $normalizedKey; + $value = (int) $value; + $newValue = $memc->decrement($internalKey, $value); + + if ($newValue === false) { + $rsCode = $memc->getResultCode(); + + // initial value + if ($rsCode == MemcachedResource::RES_NOTFOUND) { + $newValue = -$value; + $memc->add($internalKey, $newValue, $this->expirationTime()); + $rsCode = $memc->getResultCode(); + } + + if ($rsCode) { + throw $this->getExceptionByResultCode($rsCode); + } + } + + return $newValue; + } + + /* status */ + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities === null) { + $this->capabilityMarker = new stdClass(); + $this->capabilities = new Capabilities( + $this, + $this->capabilityMarker, + array( + 'supportedDatatypes' => array( + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => true, + 'object' => 'object', + 'resource' => false, + ), + 'supportedMetadata' => array(), + 'minTtl' => 1, + 'maxTtl' => 0, + 'staticTtl' => true, + 'ttlPrecision' => 1, + 'useRequestTime' => false, + 'expiredRead' => false, + 'maxKeyLength' => 255, + 'namespaceIsPrefix' => true, + ) + ); + } + + return $this->capabilities; + } + + /* internal */ + + /** + * Get expiration time by ttl + * + * Some storage commands involve sending an expiration value (relative to + * an item or to an operation requested by the client) to the server. In + * all such cases, the actual value sent may either be Unix time (number of + * seconds since January 1, 1970, as an integer), or a number of seconds + * starting from current time. In the latter case, this number of seconds + * may not exceed 60*60*24*30 (number of seconds in 30 days); if the + * expiration value is larger than that, the server will consider it to be + * real Unix time value rather than an offset from current time. + * + * @return int + */ + protected function expirationTime() + { + $ttl = $this->getOptions()->getTtl(); + if ($ttl > 2592000) { + return time() + $ttl; + } + return $ttl; + } + + /** + * Generate exception based of memcached result code + * + * @param int $code + * @return Exception\RuntimeException + * @throws Exception\InvalidArgumentException On success code + */ + protected function getExceptionByResultCode($code) + { + switch ($code) { + case MemcachedResource::RES_SUCCESS: + throw new Exception\InvalidArgumentException( + "The result code '{$code}' (SUCCESS) isn't an error" + ); + + default: + return new Exception\RuntimeException($this->getMemcachedResource()->getResultMessage()); + } + } +} diff --git a/library/Zend/Cache/Storage/Adapter/MemcachedOptions.php b/library/Zend/Cache/Storage/Adapter/MemcachedOptions.php new file mode 100755 index 0000000000..fabf3033df --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/MemcachedOptions.php @@ -0,0 +1,319 @@ +namespaceSeparator !== $namespaceSeparator) { + $this->triggerOptionEvent('namespace_separator', $namespaceSeparator); + $this->namespaceSeparator = $namespaceSeparator; + } + return $this; + } + + /** + * Get namespace separator + * + * @return string + */ + public function getNamespaceSeparator() + { + return $this->namespaceSeparator; + } + + /** + * A memcached resource to share + * + * @param null|MemcachedResource $memcachedResource + * @return MemcachedOptions + * @deprecated Please use the resource manager instead + */ + public function setMemcachedResource(MemcachedResource $memcachedResource = null) + { + trigger_error( + 'This method is deprecated and will be removed in the feature' + . ', please use the resource manager instead', + E_USER_DEPRECATED + ); + + if ($memcachedResource !== null) { + $this->triggerOptionEvent('memcached_resource', $memcachedResource); + $resourceManager = $this->getResourceManager(); + $resourceId = $this->getResourceId(); + $resourceManager->setResource($resourceId, $memcachedResource); + } + return $this; + } + + /** + * Get memcached resource to share + * + * @return MemcachedResource + * @deprecated Please use the resource manager instead + */ + public function getMemcachedResource() + { + trigger_error( + 'This method is deprecated and will be removed in the feature' + . ', please use the resource manager instead', + E_USER_DEPRECATED + ); + + return $this->resourceManager->getResource($this->getResourceId()); + } + + /** + * Set the memcached resource manager to use + * + * @param null|MemcachedResourceManager $resourceManager + * @return MemcachedOptions + */ + public function setResourceManager(MemcachedResourceManager $resourceManager = null) + { + if ($this->resourceManager !== $resourceManager) { + $this->triggerOptionEvent('resource_manager', $resourceManager); + $this->resourceManager = $resourceManager; + } + return $this; + } + + /** + * Get the memcached resource manager + * + * @return MemcachedResourceManager + */ + public function getResourceManager() + { + if (!$this->resourceManager) { + $this->resourceManager = new MemcachedResourceManager(); + } + return $this->resourceManager; + } + + /** + * Get the memcached resource id + * + * @return string + */ + public function getResourceId() + { + return $this->resourceId; + } + + /** + * Set the memcached resource id + * + * @param string $resourceId + * @return MemcachedOptions + */ + public function setResourceId($resourceId) + { + $resourceId = (string) $resourceId; + if ($this->resourceId !== $resourceId) { + $this->triggerOptionEvent('resource_id', $resourceId); + $this->resourceId = $resourceId; + } + return $this; + } + + /** + * Get the persistent id + * + * @return string + */ + public function getPersistentId() + { + return $this->getResourceManager()->getPersistentId($this->getResourceId()); + } + + /** + * Set the persistent id + * + * @param string $persistentId + * @return MemcachedOptions + */ + public function setPersistentId($persistentId) + { + $this->triggerOptionEvent('persistent_id', $persistentId); + $this->getResourceManager()->setPersistentId($this->getResourceId(), $persistentId); + return $this; + } + + /** + * Add a server to the list + * + * @param string $host + * @param int $port + * @param int $weight + * @return MemcachedOptions + * @deprecated Please use the resource manager instead + */ + public function addServer($host, $port = 11211, $weight = 0) + { + trigger_error( + 'This method is deprecated and will be removed in the feature' + . ', please use the resource manager instead', + E_USER_DEPRECATED + ); + + $this->getResourceManager()->addServer($this->getResourceId(), array( + 'host' => $host, + 'port' => $port, + 'weight' => $weight + )); + + return $this; + } + + /** + * Set a list of memcached servers to add on initialize + * + * @param string|array $servers list of servers + * @return MemcachedOptions + * @throws Exception\InvalidArgumentException + */ + public function setServers($servers) + { + $this->getResourceManager()->setServers($this->getResourceId(), $servers); + return $this; + } + + /** + * Get Servers + * + * @return array + */ + public function getServers() + { + return $this->getResourceManager()->getServers($this->getResourceId()); + } + + /** + * Set libmemcached options + * + * @param array $libOptions + * @return MemcachedOptions + * @link http://php.net/manual/memcached.constants.php + */ + public function setLibOptions(array $libOptions) + { + $this->getResourceManager()->setLibOptions($this->getResourceId(), $libOptions); + return $this; + } + + /** + * Set libmemcached option + * + * @param string|int $key + * @param mixed $value + * @return MemcachedOptions + * @link http://php.net/manual/memcached.constants.php + * @deprecated Please use lib_options or the resource manager instead + */ + public function setLibOption($key, $value) + { + trigger_error( + 'This method is deprecated and will be removed in the feature' + . ', please use "lib_options" or the resource manager instead', + E_USER_DEPRECATED + ); + + $this->getResourceManager()->setLibOption($this->getResourceId(), $key, $value); + return $this; + } + + /** + * Get libmemcached options + * + * @return array + * @link http://php.net/manual/memcached.constants.php + */ + public function getLibOptions() + { + return $this->getResourceManager()->getLibOptions($this->getResourceId()); + } + + /** + * Get libmemcached option + * + * @param string|int $key + * @return mixed + * @link http://php.net/manual/memcached.constants.php + * @deprecated Please use lib_options or the resource manager instead + */ + public function getLibOption($key) + { + trigger_error( + 'This method is deprecated and will be removed in the feature' + . ', please use "lib_options" or the resource manager instead', + E_USER_DEPRECATED + ); + + return $this->getResourceManager()->getLibOption($this->getResourceId(), $key); + } +} diff --git a/library/Zend/Cache/Storage/Adapter/MemcachedResourceManager.php b/library/Zend/Cache/Storage/Adapter/MemcachedResourceManager.php new file mode 100755 index 0000000000..10edb99aef --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/MemcachedResourceManager.php @@ -0,0 +1,547 @@ + , 'port' => , 'weight' => ) + */ + public function getServers($id) + { + if (!$this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + $resource = & $this->resources[$id]; + + if ($resource instanceof MemcachedResource) { + return $resource->getServerList(); + } + return $resource['servers']; + } + + /** + * Normalize one server into the following format: + * array('host' => , 'port' => , 'weight' => ) + * + * @param string|array &$server + * @throws Exception\InvalidArgumentException + */ + protected function normalizeServer(&$server) + { + $host = null; + $port = 11211; + $weight = 0; + + // convert a single server into an array + if ($server instanceof Traversable) { + $server = ArrayUtils::iteratorToArray($server); + } + + if (is_array($server)) { + // array([, [, ]]) + if (isset($server[0])) { + $host = (string) $server[0]; + $port = isset($server[1]) ? (int) $server[1] : $port; + $weight = isset($server[2]) ? (int) $server[2] : $weight; + } + + // array('host' => [, 'port' => [, 'weight' => ]]) + if (!isset($server[0]) && isset($server['host'])) { + $host = (string) $server['host']; + $port = isset($server['port']) ? (int) $server['port'] : $port; + $weight = isset($server['weight']) ? (int) $server['weight'] : $weight; + } + } else { + // parse server from URI host{:?port}{?weight} + $server = trim($server); + if (strpos($server, '://') === false) { + $server = 'tcp://' . $server; + } + + $server = parse_url($server); + if (!$server) { + throw new Exception\InvalidArgumentException("Invalid server given"); + } + + $host = $server['host']; + $port = isset($server['port']) ? (int) $server['port'] : $port; + + if (isset($server['query'])) { + $query = null; + parse_str($server['query'], $query); + if (isset($query['weight'])) { + $weight = (int) $query['weight']; + } + } + } + + if (!$host) { + throw new Exception\InvalidArgumentException('Missing required server host'); + } + + $server = array( + 'host' => $host, + 'port' => $port, + 'weight' => $weight, + ); + } + + /** + * Check if a resource exists + * + * @param string $id + * @return bool + */ + public function hasResource($id) + { + return isset($this->resources[$id]); + } + + /** + * Gets a memcached resource + * + * @param string $id + * @return MemcachedResource + * @throws Exception\RuntimeException + */ + public function getResource($id) + { + if (!$this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + $resource = $this->resources[$id]; + if ($resource instanceof MemcachedResource) { + return $resource; + } + + if ($resource['persistent_id'] !== '') { + $memc = new MemcachedResource($resource['persistent_id']); + } else { + $memc = new MemcachedResource(); + } + + if (method_exists($memc, 'setOptions')) { + $memc->setOptions($resource['lib_options']); + } else { + foreach ($resource['lib_options'] as $k => $v) { + $memc->setOption($k, $v); + } + } + + // merge and add servers (with persistence id servers could be added already) + $servers = array_udiff($resource['servers'], $memc->getServerList(), array($this, 'compareServers')); + if ($servers) { + $memc->addServers($servers); + } + + // buffer and return + $this->resources[$id] = $memc; + return $memc; + } + + /** + * Set a resource + * + * @param string $id + * @param array|Traversable|MemcachedResource $resource + * @return MemcachedResourceManager Fluent interface + */ + public function setResource($id, $resource) + { + $id = (string) $id; + + if (!($resource instanceof MemcachedResource)) { + if ($resource instanceof Traversable) { + $resource = ArrayUtils::iteratorToArray($resource); + } elseif (!is_array($resource)) { + throw new Exception\InvalidArgumentException( + 'Resource must be an instance of Memcached or an array or Traversable' + ); + } + + $resource = array_merge(array( + 'persistent_id' => '', + 'lib_options' => array(), + 'servers' => array(), + ), $resource); + + // normalize and validate params + $this->normalizePersistentId($resource['persistent_id']); + $this->normalizeLibOptions($resource['lib_options']); + $this->normalizeServers($resource['servers']); + } + + $this->resources[$id] = $resource; + return $this; + } + + /** + * Remove a resource + * + * @param string $id + * @return MemcachedResourceManager Fluent interface + */ + public function removeResource($id) + { + unset($this->resources[$id]); + return $this; + } + + /** + * Set the persistent id + * + * @param string $id + * @param string $persistentId + * @return MemcachedResourceManager Fluent interface + * @throws Exception\RuntimeException + */ + public function setPersistentId($id, $persistentId) + { + if (!$this->hasResource($id)) { + return $this->setResource($id, array( + 'persistent_id' => $persistentId + )); + } + + $resource = & $this->resources[$id]; + if ($resource instanceof MemcachedResource) { + throw new Exception\RuntimeException( + "Can't change persistent id of resource {$id} after instanziation" + ); + } + + $this->normalizePersistentId($persistentId); + $resource['persistent_id'] = $persistentId; + + return $this; + } + + /** + * Get the persistent id + * + * @param string $id + * @return string + * @throws Exception\RuntimeException + */ + public function getPersistentId($id) + { + if (!$this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + $resource = & $this->resources[$id]; + + if ($resource instanceof MemcachedResource) { + throw new Exception\RuntimeException( + "Can't get persistent id of an instantiated memcached resource" + ); + } + + return $resource['persistent_id']; + } + + /** + * Normalize the persistent id + * + * @param string $persistentId + */ + protected function normalizePersistentId(& $persistentId) + { + $persistentId = (string) $persistentId; + } + + /** + * Set Libmemcached options + * + * @param string $id + * @param array $libOptions + * @return MemcachedResourceManager Fluent interface + */ + public function setLibOptions($id, array $libOptions) + { + if (!$this->hasResource($id)) { + return $this->setResource($id, array( + 'lib_options' => $libOptions + )); + } + + $this->normalizeLibOptions($libOptions); + + $resource = & $this->resources[$id]; + if ($resource instanceof MemcachedResource) { + if (method_exists($resource, 'setOptions')) { + $resource->setOptions($libOptions); + } else { + foreach ($libOptions as $key => $value) { + $resource->setOption($key, $value); + } + } + } else { + $resource['lib_options'] = $libOptions; + } + + return $this; + } + + /** + * Get Libmemcached options + * + * @param string $id + * @return array + * @throws Exception\RuntimeException + */ + public function getLibOptions($id) + { + if (!$this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + $resource = & $this->resources[$id]; + + if ($resource instanceof MemcachedResource) { + $libOptions = array(); + $reflection = new ReflectionClass('Memcached'); + $constants = $reflection->getConstants(); + foreach ($constants as $constName => $constValue) { + if (substr($constName, 0, 4) == 'OPT_') { + $libOptions[$constValue] = $resource->getOption($constValue); + } + } + return $libOptions; + } + return $resource['lib_options']; + } + + /** + * Set one Libmemcached option + * + * @param string $id + * @param string|int $key + * @param mixed $value + * @return MemcachedResourceManager Fluent interface + */ + public function setLibOption($id, $key, $value) + { + return $this->setLibOptions($id, array($key => $value)); + } + + /** + * Get one Libmemcached option + * + * @param string $id + * @param string|int $key + * @return mixed + * @throws Exception\RuntimeException + */ + public function getLibOption($id, $key) + { + if (!$this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + $this->normalizeLibOptionKey($key); + $resource = & $this->resources[$id]; + + if ($resource instanceof MemcachedResource) { + return $resource->getOption($key); + } + + return isset($resource['lib_options'][$key]) ? $resource['lib_options'][$key] : null; + } + + /** + * Normalize libmemcached options + * + * @param array|Traversable $libOptions + * @throws Exception\InvalidArgumentException + */ + protected function normalizeLibOptions(& $libOptions) + { + if (!is_array($libOptions) && !($libOptions instanceof Traversable)) { + throw new Exception\InvalidArgumentException( + "Lib-Options must be an array or an instance of Traversable" + ); + } + + $result = array(); + foreach ($libOptions as $key => $value) { + $this->normalizeLibOptionKey($key); + $result[$key] = $value; + } + + $libOptions = $result; + } + + /** + * Convert option name into it's constant value + * + * @param string|int $key + * @throws Exception\InvalidArgumentException + */ + protected function normalizeLibOptionKey(& $key) + { + // convert option name into it's constant value + if (is_string($key)) { + $const = 'Memcached::OPT_' . str_replace(array(' ', '-'), '_', strtoupper($key)); + if (!defined($const)) { + throw new Exception\InvalidArgumentException("Unknown libmemcached option '{$key}' ({$const})"); + } + $key = constant($const); + } else { + $key = (int) $key; + } + } + + /** + * Set servers + * + * $servers can be an array list or a comma separated list of servers. + * One server in the list can be descripted as follows: + * - URI: [tcp://][:][?weight=] + * - Assoc: array('host' => [, 'port' => ][, 'weight' => ]) + * - List: array([, ][, ]) + * + * @param string $id + * @param string|array $servers + * @return MemcachedResourceManager + */ + public function setServers($id, $servers) + { + if (!$this->hasResource($id)) { + return $this->setResource($id, array( + 'servers' => $servers + )); + } + + $this->normalizeServers($servers); + + $resource = & $this->resources[$id]; + if ($resource instanceof MemcachedResource) { + // don't add servers twice + $servers = array_udiff($servers, $resource->getServerList(), array($this, 'compareServers')); + if ($servers) { + $resource->addServers($servers); + } + } else { + $resource['servers'] = $servers; + } + + return $this; + } + + /** + * Add servers + * + * @param string $id + * @param string|array $servers + * @return MemcachedResourceManager + */ + public function addServers($id, $servers) + { + if (!$this->hasResource($id)) { + return $this->setResource($id, array( + 'servers' => $servers + )); + } + + $this->normalizeServers($servers); + + $resource = & $this->resources[$id]; + if ($resource instanceof MemcachedResource) { + // don't add servers twice + $servers = array_udiff($servers, $resource->getServerList(), array($this, 'compareServers')); + if ($servers) { + $resource->addServers($servers); + } + } else { + // don't add servers twice + $resource['servers'] = array_merge( + $resource['servers'], + array_udiff($servers, $resource['servers'], array($this, 'compareServers')) + ); + } + + return $this; + } + + /** + * Add one server + * + * @param string $id + * @param string|array $server + * @return MemcachedResourceManager + */ + public function addServer($id, $server) + { + return $this->addServers($id, array($server)); + } + + /** + * Normalize a list of servers into the following format: + * array(array('host' => , 'port' => , 'weight' => )[, ...]) + * + * @param string|array $servers + */ + protected function normalizeServers(& $servers) + { + if (!is_array($servers) && !$servers instanceof Traversable) { + // Convert string into a list of servers + $servers = explode(',', $servers); + } + + $result = array(); + foreach ($servers as $server) { + $this->normalizeServer($server); + $result[$server['host'] . ':' . $server['port']] = $server; + } + + $servers = array_values($result); + } + + /** + * Compare 2 normalized server arrays + * (Compares only the host and the port) + * + * @param array $serverA + * @param array $serverB + * @return int + */ + protected function compareServers(array $serverA, array $serverB) + { + $keyA = $serverA['host'] . ':' . $serverA['port']; + $keyB = $serverB['host'] . ':' . $serverB['port']; + if ($keyA === $keyB) { + return 0; + } + return $keyA > $keyB ? 1 : -1; + } +} diff --git a/library/Zend/Cache/Storage/Adapter/Memory.php b/library/Zend/Cache/Storage/Adapter/Memory.php new file mode 100755 index 0000000000..85aa32e6b4 --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/Memory.php @@ -0,0 +1,747 @@ + => array( + * => array( + * 0 => + * 1 => + * ['tags' => ] + * ) + * ) + * ) + * + * @var array + */ + protected $data = array(); + + /** + * Set options. + * + * @param array|\Traversable|MemoryOptions $options + * @return Memory + * @see getOptions() + */ + public function setOptions($options) + { + if (!$options instanceof MemoryOptions) { + $options = new MemoryOptions($options); + } + + return parent::setOptions($options); + } + + /** + * Get options. + * + * @return MemoryOptions + * @see setOptions() + */ + public function getOptions() + { + if (!$this->options) { + $this->setOptions(new MemoryOptions()); + } + return $this->options; + } + + /* TotalSpaceCapableInterface */ + + /** + * Get total space in bytes + * + * @return int|float + */ + public function getTotalSpace() + { + return $this->getOptions()->getMemoryLimit(); + } + + /* AvailableSpaceCapableInterface */ + + /** + * Get available space in bytes + * + * @return int|float + */ + public function getAvailableSpace() + { + $total = $this->getOptions()->getMemoryLimit(); + $avail = $total - (float) memory_get_usage(true); + return ($avail > 0) ? $avail : 0; + } + + /* IterableInterface */ + + /** + * Get the storage iterator + * + * @return KeyListIterator + */ + public function getIterator() + { + $ns = $this->getOptions()->getNamespace(); + $keys = array(); + + if (isset($this->data[$ns])) { + foreach ($this->data[$ns] as $key => & $tmp) { + if ($this->internalHasItem($key)) { + $keys[] = $key; + } + } + } + + return new KeyListIterator($this, $keys); + } + + /* FlushableInterface */ + + /** + * Flush the whole storage + * + * @return bool + */ + public function flush() + { + $this->data = array(); + return true; + } + + /* ClearExpiredInterface */ + + /** + * Remove expired items + * + * @return bool + */ + public function clearExpired() + { + $ttl = $this->getOptions()->getTtl(); + if ($ttl <= 0) { + return true; + } + + $ns = $this->getOptions()->getNamespace(); + if (!isset($this->data[$ns])) { + return true; + } + + $data = & $this->data[$ns]; + foreach ($data as $key => & $item) { + if (microtime(true) >= $data[$key][1] + $ttl) { + unset($data[$key]); + } + } + + return true; + } + + /* ClearByNamespaceInterface */ + + public function clearByNamespace($namespace) + { + $namespace = (string) $namespace; + if ($namespace === '') { + throw new Exception\InvalidArgumentException('No namespace given'); + } + + unset($this->data[$namespace]); + return true; + } + + /* ClearByPrefixInterface */ + + /** + * Remove items matching given prefix + * + * @param string $prefix + * @return bool + */ + public function clearByPrefix($prefix) + { + $prefix = (string) $prefix; + if ($prefix === '') { + throw new Exception\InvalidArgumentException('No prefix given'); + } + + $ns = $this->getOptions()->getNamespace(); + if (!isset($this->data[$ns])) { + return true; + } + + $prefixL = strlen($prefix); + $data = & $this->data[$ns]; + foreach ($data as $key => & $item) { + if (substr($key, 0, $prefixL) === $prefix) { + unset($data[$key]); + } + } + + return true; + } + + /* TaggableInterface */ + + /** + * Set tags to an item by given key. + * An empty array will remove all tags. + * + * @param string $key + * @param string[] $tags + * @return bool + */ + public function setTags($key, array $tags) + { + $ns = $this->getOptions()->getNamespace(); + if (!isset($this->data[$ns][$key])) { + return false; + } + + $this->data[$ns][$key]['tags'] = $tags; + return true; + } + + /** + * Get tags of an item by given key + * + * @param string $key + * @return string[]|FALSE + */ + public function getTags($key) + { + $ns = $this->getOptions()->getNamespace(); + if (!isset($this->data[$ns][$key])) { + return false; + } + + return isset($this->data[$ns][$key]['tags']) ? $this->data[$ns][$key]['tags'] : array(); + } + + /** + * Remove items matching given tags. + * + * If $disjunction only one of the given tags must match + * else all given tags must match. + * + * @param string[] $tags + * @param bool $disjunction + * @return bool + */ + public function clearByTags(array $tags, $disjunction = false) + { + $ns = $this->getOptions()->getNamespace(); + if (!isset($this->data[$ns])) { + return true; + } + + $tagCount = count($tags); + $data = & $this->data[$ns]; + foreach ($data as $key => & $item) { + if (isset($item['tags'])) { + $diff = array_diff($tags, $item['tags']); + if (($disjunction && count($diff) < $tagCount) || (!$disjunction && !$diff)) { + unset($data[$key]); + } + } + } + + return true; + } + + /* reading */ + + /** + * Internal method to get an item. + * + * @param string $normalizedKey + * @param bool $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) + { + $options = $this->getOptions(); + $ns = $options->getNamespace(); + $success = isset($this->data[$ns][$normalizedKey]); + if ($success) { + $data = & $this->data[$ns][$normalizedKey]; + $ttl = $options->getTtl(); + if ($ttl && microtime(true) >= ($data[1] + $ttl)) { + $success = false; + } + } + + if (!$success) { + return null; + } + + $casToken = $data[0]; + return $data[0]; + } + + /** + * Internal method to get multiple items. + * + * @param array $normalizedKeys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + */ + protected function internalGetItems(array & $normalizedKeys) + { + $options = $this->getOptions(); + $ns = $options->getNamespace(); + if (!isset($this->data[$ns])) { + return array(); + } + + $data = & $this->data[$ns]; + $ttl = $options->getTtl(); + $now = microtime(true); + + $result = array(); + foreach ($normalizedKeys as $normalizedKey) { + if (isset($data[$normalizedKey])) { + if (!$ttl || $now < ($data[$normalizedKey][1] + $ttl)) { + $result[$normalizedKey] = $data[$normalizedKey][0]; + } + } + } + + return $result; + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @return bool + */ + protected function internalHasItem(& $normalizedKey) + { + $options = $this->getOptions(); + $ns = $options->getNamespace(); + if (!isset($this->data[$ns][$normalizedKey])) { + return false; + } + + // check if expired + $ttl = $options->getTtl(); + if ($ttl && microtime(true) >= ($this->data[$ns][$normalizedKey][1] + $ttl)) { + return false; + } + + return true; + } + + /** + * Internal method to test multiple items. + * + * @param array $normalizedKeys + * @return array Array of found keys + */ + protected function internalHasItems(array & $normalizedKeys) + { + $options = $this->getOptions(); + $ns = $options->getNamespace(); + if (!isset($this->data[$ns])) { + return array(); + } + + $data = & $this->data[$ns]; + $ttl = $options->getTtl(); + $now = microtime(true); + + $result = array(); + foreach ($normalizedKeys as $normalizedKey) { + if (isset($data[$normalizedKey])) { + if (!$ttl || $now < ($data[$normalizedKey][1] + $ttl)) { + $result[] = $normalizedKey; + } + } + } + + return $result; + } + + /** + * Get metadata of an item. + * + * @param string $normalizedKey + * @return array|bool Metadata on success, false on failure + * @throws Exception\ExceptionInterface + * + * @triggers getMetadata.pre(PreEvent) + * @triggers getMetadata.post(PostEvent) + * @triggers getMetadata.exception(ExceptionEvent) + */ + protected function internalGetMetadata(& $normalizedKey) + { + if (!$this->internalHasItem($normalizedKey)) { + return false; + } + + $ns = $this->getOptions()->getNamespace(); + return array( + 'mtime' => $this->data[$ns][$normalizedKey][1], + ); + } + + /* writing */ + + /** + * Internal method to store an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + + if (!$this->hasAvailableSpace()) { + $memoryLimit = $options->getMemoryLimit(); + throw new Exception\OutOfSpaceException( + "Memory usage exceeds limit ({$memoryLimit})." + ); + } + + $ns = $options->getNamespace(); + $this->data[$ns][$normalizedKey] = array($value, microtime(true)); + + return true; + } + + /** + * Internal method to store multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalSetItems(array & $normalizedKeyValuePairs) + { + $options = $this->getOptions(); + + if (!$this->hasAvailableSpace()) { + $memoryLimit = $options->getMemoryLimit(); + throw new Exception\OutOfSpaceException( + "Memory usage exceeds limit ({$memoryLimit})." + ); + } + + $ns = $options->getNamespace(); + if (!isset($this->data[$ns])) { + $this->data[$ns] = array(); + } + + $data = & $this->data[$ns]; + $now = microtime(true); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + $data[$normalizedKey] = array($value, $now); + } + + return array(); + } + + /** + * Add an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalAddItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + + if (!$this->hasAvailableSpace()) { + $memoryLimit = $options->getMemoryLimit(); + throw new Exception\OutOfSpaceException( + "Memory usage exceeds limit ({$memoryLimit})." + ); + } + + $ns = $options->getNamespace(); + if (isset($this->data[$ns][$normalizedKey])) { + return false; + } + + $this->data[$ns][$normalizedKey] = array($value, microtime(true)); + return true; + } + + /** + * Internal method to add multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalAddItems(array & $normalizedKeyValuePairs) + { + $options = $this->getOptions(); + + if (!$this->hasAvailableSpace()) { + $memoryLimit = $options->getMemoryLimit(); + throw new Exception\OutOfSpaceException( + "Memory usage exceeds limit ({$memoryLimit})." + ); + } + + $ns = $options->getNamespace(); + if (!isset($this->data[$ns])) { + $this->data[$ns] = array(); + } + + $result = array(); + $data = & $this->data[$ns]; + $now = microtime(true); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + if (isset($data[$normalizedKey])) { + $result[] = $normalizedKey; + } else { + $data[$normalizedKey] = array($value, $now); + } + } + + return $result; + } + + /** + * Internal method to replace an existing item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalReplaceItem(& $normalizedKey, & $value) + { + $ns = $this->getOptions()->getNamespace(); + if (!isset($this->data[$ns][$normalizedKey])) { + return false; + } + $this->data[$ns][$normalizedKey] = array($value, microtime(true)); + + return true; + } + + /** + * Internal method to replace multiple existing items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalReplaceItems(array & $normalizedKeyValuePairs) + { + $ns = $this->getOptions()->getNamespace(); + if (!isset($this->data[$ns])) { + return array_keys($normalizedKeyValuePairs); + } + + $result = array(); + $data = & $this->data[$ns]; + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + if (!isset($data[$normalizedKey])) { + $result[] = $normalizedKey; + } else { + $data[$normalizedKey] = array($value, microtime(true)); + } + } + + return $result; + } + + /** + * Internal method to reset lifetime of an item + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalTouchItem(& $normalizedKey) + { + $ns = $this->getOptions()->getNamespace(); + + if (!isset($this->data[$ns][$normalizedKey])) { + return false; + } + + $this->data[$ns][$normalizedKey][1] = microtime(true); + return true; + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItem(& $normalizedKey) + { + $ns = $this->getOptions()->getNamespace(); + if (!isset($this->data[$ns][$normalizedKey])) { + return false; + } + + unset($this->data[$ns][$normalizedKey]); + + // remove empty namespace + if (!$this->data[$ns]) { + unset($this->data[$ns]); + } + + return true; + } + + /** + * Internal method to increment an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalIncrementItem(& $normalizedKey, & $value) + { + $ns = $this->getOptions()->getNamespace(); + if (isset($this->data[$ns][$normalizedKey])) { + $data = & $this->data[$ns][$normalizedKey]; + $data[0]+= $value; + $data[1] = microtime(true); + $newValue = $data[0]; + } else { + // initial value + $newValue = $value; + $this->data[$ns][$normalizedKey] = array($newValue, microtime(true)); + } + + return $newValue; + } + + /** + * Internal method to decrement an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalDecrementItem(& $normalizedKey, & $value) + { + $ns = $this->getOptions()->getNamespace(); + if (isset($this->data[$ns][$normalizedKey])) { + $data = & $this->data[$ns][$normalizedKey]; + $data[0]-= $value; + $data[1] = microtime(true); + $newValue = $data[0]; + } else { + // initial value + $newValue = -$value; + $this->data[$ns][$normalizedKey] = array($newValue, microtime(true)); + } + + return $newValue; + } + + /* status */ + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities === null) { + $this->capabilityMarker = new stdClass(); + $this->capabilities = new Capabilities( + $this, + $this->capabilityMarker, + array( + 'supportedDatatypes' => array( + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => true, + 'object' => true, + 'resource' => true, + ), + 'supportedMetadata' => array('mtime'), + 'minTtl' => 1, + 'maxTtl' => PHP_INT_MAX, + 'staticTtl' => false, + 'ttlPrecision' => 0.05, + 'expiredRead' => true, + 'maxKeyLength' => 0, + 'namespaceIsPrefix' => false, + 'namespaceSeparator' => '', + ) + ); + } + + return $this->capabilities; + } + + /* internal */ + + /** + * Has space available to store items? + * + * @return bool + */ + protected function hasAvailableSpace() + { + $total = $this->getOptions()->getMemoryLimit(); + + // check memory limit disabled + if ($total <= 0) { + return true; + } + + $free = $total - (float) memory_get_usage(true); + return ($free > 0); + } +} diff --git a/library/Zend/Cache/Storage/Adapter/MemoryOptions.php b/library/Zend/Cache/Storage/Adapter/MemoryOptions.php new file mode 100755 index 0000000000..be48418c64 --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/MemoryOptions.php @@ -0,0 +1,112 @@ +normalizeMemoryLimit($memoryLimit); + + if ($this->memoryLimit != $memoryLimit) { + $this->triggerOptionEvent('memory_limit', $memoryLimit); + $this->memoryLimit = $memoryLimit; + } + + return $this; + } + + /** + * Get memory limit + * + * If the used memory of PHP exceeds this limit an OutOfSpaceException + * will be thrown. + * + * @return int + */ + public function getMemoryLimit() + { + if ($this->memoryLimit === null) { + // By default use half of PHP's memory limit if possible + $memoryLimit = $this->normalizeMemoryLimit(ini_get('memory_limit')); + if ($memoryLimit >= 0) { + $this->memoryLimit = (int) ($memoryLimit / 2); + } else { + // disable memory limit + $this->memoryLimit = 0; + } + } + + return $this->memoryLimit; + } + + /** + * Normalized a given value of memory limit into the number of bytes + * + * @param string|int $value + * @throws Exception\InvalidArgumentException + * @return int + */ + protected function normalizeMemoryLimit($value) + { + if (is_numeric($value)) { + return (int) $value; + } + + if (!preg_match('/(\-?\d+)\s*(\w*)/', ini_get('memory_limit'), $matches)) { + throw new Exception\InvalidArgumentException("Invalid memory limit '{$value}'"); + } + + $value = (int) $matches[1]; + if ($value <= 0) { + return 0; + } + + switch (strtoupper($matches[2])) { + case 'G': + $value*= 1024; + // no break + + case 'M': + $value*= 1024; + // no break + + case 'K': + $value*= 1024; + // no break + } + + return $value; + } +} diff --git a/library/Zend/Cache/Storage/Adapter/Redis.php b/library/Zend/Cache/Storage/Adapter/Redis.php new file mode 100755 index 0000000000..1adb3d4a41 --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/Redis.php @@ -0,0 +1,433 @@ +initialized; + $this->getEventManager()->attach('option', function ($event) use (& $initialized) { + $initialized = false; + }); + } + + /** + * Get Redis resource + * + * @return RedisResource + */ + protected function getRedisResource() + { + if (!$this->initialized) { + $options = $this->getOptions(); + + // get resource manager and resource id + $this->resourceManager = $options->getResourceManager(); + $this->resourceId = $options->getResourceId(); + + // init namespace prefix + $namespace = $options->getNamespace(); + if ($namespace !== '') { + $this->namespacePrefix = $namespace . $options->getNamespaceSeparator(); + } else { + $this->namespacePrefix = ''; + } + + // update initialized flag + $this->initialized = true; + } + + return $this->resourceManager->getResource($this->resourceId); + } + + /* options */ + + /** + * Set options. + * + * @param array|Traversable|RedisOptions $options + * @return Redis + * @see getOptions() + */ + public function setOptions($options) + { + if (!$options instanceof RedisOptions) { + $options = new RedisOptions($options); + } + return parent::setOptions($options); + } + + /** + * Get options. + * + * @return RedisOptions + * @see setOptions() + */ + public function getOptions() + { + if (!$this->options) { + $this->setOptions(new RedisOptions()); + } + return $this->options; + } + + /** + * Internal method to get an item. + * + * @param string &$normalizedKey Key where to store data + * @param bool &$success If the operation was successfull + * @param mixed &$casToken Token + * @return mixed Data on success, false on key not found + * @throws Exception\RuntimeException + */ + protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) + { + $redis = $this->getRedisResource(); + try { + $value = $redis->get($this->namespacePrefix . $normalizedKey); + } catch (RedisResourceException $e) { + throw new Exception\RuntimeException($redis->getLastError(), $e->getCode(), $e); + } + + if ($value === false) { + $success = false; + return null; + } + + $success = true; + $casToken = $value; + return $value; + } + + /** + * Internal method to get multiple items. + * + * @param array &$normalizedKeys Array of keys to be obtained + * + * @return array Associative array of keys and values + * @throws Exception\RuntimeException + */ + protected function internalGetItems(array & $normalizedKeys) + { + $redis = $this->getRedisResource(); + + $namespacedKeys = array(); + foreach ($normalizedKeys as $normalizedKey) { + $namespacedKeys[] = $this->namespacePrefix . $normalizedKey; + } + + try { + $results = $redis->mGet($namespacedKeys); + } catch (RedisResourceException $e) { + throw new Exception\RuntimeException($redis->getLastError(), $e->getCode(), $e); + } + //combine the key => value pairs and remove all missing values + return array_filter( + array_combine($normalizedKeys, $results), + function ($value) { + return $value !== false; + } + ); + } + + /** + * Internal method to test if an item exists. + * + * @param string &$normalizedKey Normalized key which will be checked + * + * @return bool + * @throws Exception\RuntimeException + */ + protected function internalHasItem(& $normalizedKey) + { + $redis = $this->getRedisResource(); + try { + return $redis->exists($this->namespacePrefix . $normalizedKey); + } catch (RedisResourceException $e) { + throw new Exception\RuntimeException($redis->getLastError(), $e->getCode(), $e); + } + } + + /** + * Internal method to store an item. + * + * @param string &$normalizedKey Key in Redis under which value will be saved + * @param mixed &$value Value to store under cache key + * + * @return bool + * @throws Exception\RuntimeException + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $redis = $this->getRedisResource(); + $ttl = $this->getOptions()->getTtl(); + + try { + if ($ttl) { + if ($this->resourceManager->getMajorVersion($this->resourceId) < 2) { + throw new Exception\UnsupportedMethodCallException("To use ttl you need version >= 2.0.0"); + } + $success = $redis->setex($this->namespacePrefix . $normalizedKey, $ttl, $value); + } else { + $success = $redis->set($this->namespacePrefix . $normalizedKey, $value); + } + } catch (RedisResourceException $e) { + throw new Exception\RuntimeException($redis->getLastError(), $e->getCode(), $e); + } + + return $success; + } + + /** + * Internal method to store multiple items. + * + * @param array &$normalizedKeyValuePairs An array of normalized key/value pairs + * + * @return array Array of not stored keys + * @throws Exception\RuntimeException + */ + protected function internalSetItems(array & $normalizedKeyValuePairs) + { + $redis = $this->getRedisResource(); + $ttl = $this->getOptions()->getTtl(); + + $namespacedKeyValuePairs = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + $namespacedKeyValuePairs[$this->namespacePrefix . $normalizedKey] = $value; + } + try { + if ($ttl > 0) { + //check if ttl is supported + if ($this->resourceManager->getMajorVersion($this->resourceId) < 2) { + throw new Exception\UnsupportedMethodCallException("To use ttl you need version >= 2.0.0"); + } + //mSet does not allow ttl, so use transaction + $transaction = $redis->multi(); + foreach ($namespacedKeyValuePairs as $key => $value) { + $transaction->setex($key, $ttl, $value); + } + $success = $transaction->exec(); + } else { + $success = $redis->mSet($namespacedKeyValuePairs); + } + } catch (RedisResourceException $e) { + throw new Exception\RuntimeException($redis->getLastError(), $e->getCode(), $e); + } + if (!$success) { + throw new Exception\RuntimeException($redis->getLastError()); + } + + return array(); + } + + /** + * Add an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\RuntimeException + */ + protected function internalAddItem(& $normalizedKey, & $value) + { + $redis = $this->getRedisResource(); + try { + return $redis->setnx($this->namespacePrefix . $normalizedKey, $value); + } catch (RedisResourceException $e) { + throw new Exception\RuntimeException($redis->getLastError(), $e->getCode(), $e); + } + } + + /** + * Internal method to remove an item. + * + * @param string &$normalizedKey Key which will be removed + * + * @return bool + * @throws Exception\RuntimeException + */ + protected function internalRemoveItem(& $normalizedKey) + { + $redis = $this->getRedisResource(); + try { + return (bool) $redis->delete($this->namespacePrefix . $normalizedKey); + } catch (RedisResourceException $e) { + throw new Exception\RuntimeException($redis->getLastError(), $e->getCode(), $e); + } + } + + /** + * Internal method to increment an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\RuntimeException + */ + protected function internalIncrementItem(& $normalizedKey, & $value) + { + $redis = $this->getRedisResource(); + try { + return $redis->incrBy($this->namespacePrefix . $normalizedKey, $value); + } catch (RedisResourceException $e) { + throw new Exception\RuntimeException($redis->getLastError(), $e->getCode(), $e); + } + } + + /** + * Internal method to decrement an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\RuntimeException + */ + protected function internalDecrementItem(& $normalizedKey, & $value) + { + $redis = $this->getRedisResource(); + try { + return $redis->decrBy($this->namespacePrefix . $normalizedKey, $value); + } catch (RedisResourceException $e) { + throw new Exception\RuntimeException($redis->getLastError(), $e->getCode(), $e); + } + } + + /** + * Flush currently set DB + * + * @return bool + * @throws Exception\RuntimeException + */ + public function flush() + { + $redis = $this->getRedisResource(); + try { + return $redis->flushDB(); + } catch (RedisResourceException $e) { + throw new Exception\RuntimeException($redis->getLastError(), $e->getCode(), $e); + } + } + + /* TotalSpaceCapableInterface */ + + /** + * Get total space in bytes + * + * @return int|float + */ + public function getTotalSpace() + { + $redis = $this->getRedisResource(); + try { + $info = $redis->info(); + } catch (RedisResourceException $e) { + throw new Exception\RuntimeException($redis->getLastError(), $e->getCode(), $e); + } + + return $info['used_memory']; + } + + /* status */ + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities === null) { + $this->capabilityMarker = new stdClass(); + $minTtl = $this->resourceManager->getMajorVersion($this->resourceId) < 2 ? 0 : 1; + //without serialization redis supports only strings for simple + //get/set methods + $this->capabilities = new Capabilities( + $this, + $this->capabilityMarker, + array( + 'supportedDatatypes' => array( + 'NULL' => 'string', + 'boolean' => 'string', + 'integer' => 'string', + 'double' => 'string', + 'string' => true, + 'array' => false, + 'object' => false, + 'resource' => false, + ), + 'supportedMetadata' => array(), + 'minTtl' => $minTtl, + 'maxTtl' => 0, + 'staticTtl' => true, + 'ttlPrecision' => 1, + 'useRequestTime' => false, + 'expiredRead' => false, + 'maxKeyLength' => 255, + 'namespaceIsPrefix' => true, + ) + ); + } + + return $this->capabilities; + } +} diff --git a/library/Zend/Cache/Storage/Adapter/RedisOptions.php b/library/Zend/Cache/Storage/Adapter/RedisOptions.php new file mode 100755 index 0000000000..f5e6748750 --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/RedisOptions.php @@ -0,0 +1,263 @@ +namespaceSeparator !== $namespaceSeparator) { + $this->triggerOptionEvent('namespace_separator', $namespaceSeparator); + $this->namespaceSeparator = $namespaceSeparator; + } + return $this; + } + + /** + * Get namespace separator + * + * @return string + */ + public function getNamespaceSeparator() + { + return $this->namespaceSeparator; + } + + /** + * Set the redis resource manager to use + * + * @param null|RedisResourceManager $resourceManager + * @return RedisOptions + */ + public function setResourceManager(RedisResourceManager $resourceManager = null) + { + if ($this->resourceManager !== $resourceManager) { + $this->triggerOptionEvent('resource_manager', $resourceManager); + $this->resourceManager = $resourceManager; + } + return $this; + } + + /** + * Get the redis resource manager + * + * @return RedisResourceManager + */ + public function getResourceManager() + { + if (!$this->resourceManager) { + $this->resourceManager = new RedisResourceManager(); + } + return $this->resourceManager; + } + + /** + * Get the redis resource id + * + * @return string + */ + public function getResourceId() + { + return $this->resourceId; + } + + /** + * Set the redis resource id + * + * @param string $resourceId + * @return RedisOptions + */ + public function setResourceId($resourceId) + { + $resourceId = (string) $resourceId; + if ($this->resourceId !== $resourceId) { + $this->triggerOptionEvent('resource_id', $resourceId); + $this->resourceId = $resourceId; + } + return $this; + } + + /** + * Get the persistent id + * + * @return string + */ + public function getPersistentId() + { + return $this->getResourceManager()->getPersistentId($this->getResourceId()); + } + + /** + * Set the persistent id + * + * @param string $persistentId + * @return RedisOptions + */ + public function setPersistentId($persistentId) + { + $this->triggerOptionEvent('persistent_id', $persistentId); + $this->getResourceManager()->setPersistentId($this->getResourceId(), $persistentId); + return $this; + } + + /** + * Set redis options + * + * @param array $libOptions + * @return RedisOptions + * @link http://github.com/nicolasff/phpredis#setoption + */ + public function setLibOptions(array $libOptions) + { + $this->triggerOptionEvent('lib_option', $libOptions); + $this->getResourceManager()->setLibOptions($this->getResourceId(), $libOptions); + return $this; + } + + /** + * Get redis options + * + * @return array + * @link http://github.com/nicolasff/phpredis#setoption + */ + public function getLibOptions() + { + return $this->getResourceManager()->getLibOptions($this->getResourceId()); + } + + /** + * Set server + * + * Server can be described as follows: + * - URI: /path/to/sock.sock + * - Assoc: array('host' => [, 'port' => [, 'timeout' => ]]) + * - List: array([, , [, ]]) + * + * @param string|array $server + * + * @return RedisOptions + */ + public function setServer($server) + { + $this->getResourceManager()->setServer($this->getResourceId(), $server); + return $this; + } + + /** + * Get server + * + * @return array array('host' => [, 'port' => [, 'timeout' => ]]) + */ + public function getServer() + { + return $this->getResourceManager()->getServer($this->getResourceId()); + } + + /** + * Set resource database number + * + * @param int $database Database number + * + * @return RedisOptions + */ + public function setDatabase($database) + { + $this->getResourceManager()->setDatabase($this->getResourceId(), $database); + return $this; + } + + /** + * Get resource database number + * + * @return int Database number + */ + public function getDatabase() + { + return $this->getResourceManager()->getDatabase($this->getResourceId()); + } + + /** + * Set resource password + * + * @param string $password Password + * + * @return RedisOptions + */ + public function setPassword($password) + { + $this->getResourceManager()->setPassword($this->getResourceId(), $password); + return $this; + } + + /** + * Get resource password + * + * @return string + */ + public function getPassword() + { + return $this->getResourceManager()->getPassword($this->getResourceId()); + } +} diff --git a/library/Zend/Cache/Storage/Adapter/RedisResourceManager.php b/library/Zend/Cache/Storage/Adapter/RedisResourceManager.php new file mode 100755 index 0000000000..ed8ee21479 --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/RedisResourceManager.php @@ -0,0 +1,645 @@ +resources[$id]); + } + + /** + * Get redis server version + * + * @param string $id + * @return int + * @throws Exception\RuntimeException + */ + public function getMajorVersion($id) + { + if (!$this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + $resource = & $this->resources[$id]; + return (int) $resource['version']; + } + + /** + * Get redis server version + * + * @deprecated 2.2.2 Use getMajorVersion instead + * + * @param string $id + * @return int + * @throws Exception\RuntimeException + */ + public function getMayorVersion($id) + { + return $this->getMajorVersion($id); + } + + /** + * Get redis resource database + * + * @param string $id + * @return string + */ + public function getDatabase($id) + { + if (!$this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + $resource = & $this->resources[$id]; + return $resource['database']; + } + + /** + * Get redis resource password + * + * @param string $id + * @return string + */ + public function getPassword($id) + { + if (!$this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + $resource = & $this->resources[$id]; + return $resource['password']; + } + + /** + * Gets a redis resource + * + * @param string $id + * @return RedisResource + * @throws Exception\RuntimeException + */ + public function getResource($id) + { + if (!$this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + $resource = & $this->resources[$id]; + if ($resource['resource'] instanceof RedisResource) { + //in case new server was set then connect + if (!$resource['initialized']) { + $this->connect($resource); + } + $info = $resource['resource']->info(); + $resource['version'] = $info['redis_version']; + return $resource['resource']; + } + + $redis = new RedisResource(); + + $resource['resource'] = $redis; + $this->connect($resource); + + foreach ($resource['lib_options'] as $k => $v) { + $redis->setOption($k, $v); + } + + $info = $redis->info(); + $resource['version'] = $info['redis_version']; + $this->resources[$id]['resource'] = $redis; + return $redis; + } + + /** + * Get server + * @param string $id + * @throws Exception\RuntimeException + * @return array array('host' => [, 'port' => [, 'timeout' => ]]) + */ + public function getServer($id) + { + if (!$this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + $resource = & $this->resources[$id]; + return $resource['server']; + } + + /** + * Normalize one server into the following format: + * array('host' => [, 'port' => [, 'timeout' => ]]) + * + * @param string|array $server + * + * @throws Exception\InvalidArgumentException + */ + protected function normalizeServer(&$server) + { + $host = null; + $port = null; + $timeout = 0; + + // convert a single server into an array + if ($server instanceof Traversable) { + $server = ArrayUtils::iteratorToArray($server); + } + + if (is_array($server)) { + // array([, [, ]]) + if (isset($server[0])) { + $host = (string) $server[0]; + $port = isset($server[1]) ? (int) $server[1] : $port; + $timeout = isset($server[2]) ? (int) $server[2] : $timeout; + } + + // array('host' => [, 'port' => , ['timeout' => ]]) + if (!isset($server[0]) && isset($server['host'])) { + $host = (string) $server['host']; + $port = isset($server['port']) ? (int) $server['port'] : $port; + $timeout = isset($server['timeout']) ? (int) $server['timeout'] : $timeout; + } + } else { + // parse server from URI host{:?port} + $server = trim($server); + if (strpos($server, '/') !== 0) { + //non unix domain socket connection + $server = parse_url($server); + } else { + $server = array('host' => $server); + } + if (!$server) { + throw new Exception\InvalidArgumentException("Invalid server given"); + } + + $host = $server['host']; + $port = isset($server['port']) ? (int) $server['port'] : $port; + $timeout = isset($server['timeout']) ? (int) $server['timeout'] : $timeout; + } + + if (!$host) { + throw new Exception\InvalidArgumentException('Missing required server host'); + } + + $server = array( + 'host' => $host, + 'port' => $port, + 'timeout' => $timeout, + ); + } + + /** + * Extract password to be used on connection + * + * @param mixed $resource + * @param mixed $serverUri + * + * @return string|null + */ + protected function extractPassword($resource, $serverUri) + { + if (! empty($resource['password'])) { + return $resource['password']; + } + + if (! is_string($serverUri)) { + return null; + } + + // parse server from URI host{:?port} + $server = trim($serverUri); + + if (strpos($server, '/') === 0) { + return null; + } + + //non unix domain socket connection + $server = parse_url($server); + + return isset($server['pass']) ? $server['pass'] : null; + } + + /** + * Connects to redis server + * + * + * @param array & $resource + * + * @return null + * @throws Exception\RuntimeException + */ + protected function connect(array & $resource) + { + $server = $resource['server']; + $redis = $resource['resource']; + if ($resource['persistent_id'] !== '') { + //connect or reuse persistent connection + $success = $redis->pconnect($server['host'], $server['port'], $server['timeout'], $server['persistent_id']); + } elseif ($server['port']) { + $success = $redis->connect($server['host'], $server['port'], $server['timeout']); + } elseif ($server['timeout']) { + //connect through unix domain socket + $success = $redis->connect($server['host'], $server['timeout']); + } else { + $success = $redis->connect($server['host']); + } + + if (!$success) { + throw new Exception\RuntimeException('Could not estabilish connection with Redis instance'); + } + + $resource['initialized'] = true; + if ($resource['password']) { + $redis->auth($resource['password']); + } + $redis->select($resource['database']); + } + + /** + * Set a resource + * + * @param string $id + * @param array|Traversable|RedisResource $resource + * @return RedisResourceManager Fluent interface + */ + public function setResource($id, $resource) + { + $id = (string) $id; + //TODO: how to get back redis connection info from resource? + $defaults = array( + 'persistent_id' => '', + 'lib_options' => array(), + 'server' => array(), + 'password' => '', + 'database' => 0, + 'resource' => null, + 'initialized' => false, + 'version' => 0, + ); + if (!$resource instanceof RedisResource) { + if ($resource instanceof Traversable) { + $resource = ArrayUtils::iteratorToArray($resource); + } elseif (!is_array($resource)) { + throw new Exception\InvalidArgumentException( + 'Resource must be an instance of an array or Traversable' + ); + } + + $resource = array_merge($defaults, $resource); + // normalize and validate params + $this->normalizePersistentId($resource['persistent_id']); + $this->normalizeLibOptions($resource['lib_options']); + + // #6495 note: order is important here, as `normalizeServer` applies destructive + // transformations on $resource['server'] + $resource['password'] = $this->extractPassword($resource, $resource['server']); + + $this->normalizeServer($resource['server']); + } else { + //there are two ways of determining if redis is already initialized + //with connect function: + //1) pinging server + //2) checking undocumented property socket which is available only + //after successful connect + $resource = array_merge( + $defaults, + array( + 'resource' => $resource, + 'initialized' => isset($resource->socket), + ) + ); + } + $this->resources[$id] = $resource; + return $this; + } + + /** + * Remove a resource + * + * @param string $id + * @return RedisResourceManager Fluent interface + */ + public function removeResource($id) + { + unset($this->resources[$id]); + return $this; + } + + /** + * Set the persistent id + * + * @param string $id + * @param string $persistentId + * @return RedisResourceManager Fluent interface + * @throws Exception\RuntimeException + */ + public function setPersistentId($id, $persistentId) + { + if (!$this->hasResource($id)) { + return $this->setResource($id, array( + 'persistent_id' => $persistentId + )); + } + + $resource = & $this->resources[$id]; + if ($resource instanceof RedisResource) { + throw new Exception\RuntimeException( + "Can't change persistent id of resource {$id} after instanziation" + ); + } + + $this->normalizePersistentId($persistentId); + $resource['persistent_id'] = $persistentId; + + return $this; + } + + /** + * Get the persistent id + * + * @param string $id + * @return string + * @throws Exception\RuntimeException + */ + public function getPersistentId($id) + { + if (!$this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + $resource = & $this->resources[$id]; + + if ($resource instanceof RedisResource) { + throw new Exception\RuntimeException( + "Can't get persistent id of an instantiated redis resource" + ); + } + + return $resource['persistent_id']; + } + + /** + * Normalize the persistent id + * + * @param string $persistentId + */ + protected function normalizePersistentId(& $persistentId) + { + $persistentId = (string) $persistentId; + } + + /** + * Set Redis options + * + * @param string $id + * @param array $libOptions + * @return RedisResourceManager Fluent interface + */ + public function setLibOptions($id, array $libOptions) + { + if (!$this->hasResource($id)) { + return $this->setResource($id, array( + 'lib_options' => $libOptions + )); + } + + $this->normalizeLibOptions($libOptions); + $resource = & $this->resources[$id]; + + $resource['lib_options'] = $libOptions; + + if ($resource['resource'] instanceof RedisResource) { + $redis = & $resource['resource']; + if (method_exists($redis, 'setOptions')) { + $redis->setOptions($libOptions); + } else { + foreach ($libOptions as $key => $value) { + $redis->setOption($key, $value); + } + } + } + + return $this; + } + + /** + * Get Redis options + * + * @param string $id + * @return array + * @throws Exception\RuntimeException + */ + public function getLibOptions($id) + { + if (!$this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + $resource = & $this->resources[$id]; + + if ($resource instanceof RedisResource) { + $libOptions = array(); + $reflection = new ReflectionClass('Redis'); + $constants = $reflection->getConstants(); + foreach ($constants as $constName => $constValue) { + if (substr($constName, 0, 4) == 'OPT_') { + $libOptions[$constValue] = $resource->getOption($constValue); + } + } + return $libOptions; + } + return $resource['lib_options']; + } + + /** + * Set one Redis option + * + * @param string $id + * @param string|int $key + * @param mixed $value + * @return RedisResourceManager Fluent interface + */ + public function setLibOption($id, $key, $value) + { + return $this->setLibOptions($id, array($key => $value)); + } + + /** + * Get one Redis option + * + * @param string $id + * @param string|int $key + * @return mixed + * @throws Exception\RuntimeException + */ + public function getLibOption($id, $key) + { + if (!$this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + $this->normalizeLibOptionKey($key); + $resource = & $this->resources[$id]; + + if ($resource instanceof RedisResource) { + return $resource->getOption($key); + } + + return isset($resource['lib_options'][$key]) ? $resource['lib_options'][$key] : null; + } + + /** + * Normalize Redis options + * + * @param array|Traversable $libOptions + * @throws Exception\InvalidArgumentException + */ + protected function normalizeLibOptions(& $libOptions) + { + if (!is_array($libOptions) && !($libOptions instanceof Traversable)) { + throw new Exception\InvalidArgumentException( + "Lib-Options must be an array or an instance of Traversable" + ); + } + + $result = array(); + foreach ($libOptions as $key => $value) { + $this->normalizeLibOptionKey($key); + $result[$key] = $value; + } + + $libOptions = $result; + } + + /** + * Convert option name into it's constant value + * + * @param string|int $key + * @throws Exception\InvalidArgumentException + */ + protected function normalizeLibOptionKey(& $key) + { + // convert option name into it's constant value + if (is_string($key)) { + $const = 'Redis::OPT_' . str_replace(array(' ', '-'), '_', strtoupper($key)); + if (!defined($const)) { + throw new Exception\InvalidArgumentException("Unknown redis option '{$key}' ({$const})"); + } + $key = constant($const); + } else { + $key = (int) $key; + } + } + + /** + * Set server + * + * Server can be described as follows: + * - URI: /path/to/sock.sock + * - Assoc: array('host' => [, 'port' => [, 'timeout' => ]]) + * - List: array([, , [, ]]) + * + * @param string $id + * @param string|array $server + * @return RedisResourceManager + */ + public function setServer($id, $server) + { + if (!$this->hasResource($id)) { + return $this->setResource($id, array( + 'server' => $server + )); + } + + $this->normalizeServer($server); + + $resource = & $this->resources[$id]; + $resource['password'] = $this->extractPassword($resource, $server); + + if ($resource['resource'] instanceof RedisResource) { + $resourceParams = array('server' => $server); + + if (! empty($resource['password'])) { + $resourceParams['password'] = $resource['password']; + } + + $this->setResource($id, $resourceParams); + } else { + $resource['server'] = $server; + } + + return $this; + } + + /** + * Set redis password + * + * @param string $id + * @param string $password + * @return RedisResource + */ + public function setPassword($id, $password) + { + if (!$this->hasResource($id)) { + return $this->setResource($id, array( + 'password' => $password, + )); + } + + $resource = & $this->resources[$id]; + $resource['password'] = $password; + $resource['initialized'] = false; + return $this; + } + + /** + * Set redis database number + * + * @param string $id + * @param int $database + * @return RedisResource + */ + public function setDatabase($id, $database) + { + if (!$this->hasResource($id)) { + return $this->setResource($id, array( + 'database' => (int) $database, + )); + } + + $resource = & $this->resources[$id]; + $resource['database'] = $database; + $resource['initialized'] = false; + return $this; + } +} diff --git a/library/Zend/Cache/Storage/Adapter/Session.php b/library/Zend/Cache/Storage/Adapter/Session.php new file mode 100755 index 0000000000..4603c23149 --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/Session.php @@ -0,0 +1,546 @@ +options) { + $this->setOptions(new SessionOptions()); + } + return $this->options; + } + + /** + * Get the session container + * + * @return SessionContainer + */ + protected function getSessionContainer() + { + $sessionContainer = $this->getOptions()->getSessionContainer(); + if (!$sessionContainer) { + throw new Exception\RuntimeException("No session container configured"); + } + return $sessionContainer; + } + + /* IterableInterface */ + + /** + * Get the storage iterator + * + * @return KeyListIterator + */ + public function getIterator() + { + $cntr = $this->getSessionContainer(); + $ns = $this->getOptions()->getNamespace(); + + if ($cntr->offsetExists($ns)) { + $keys = array_keys($cntr->offsetGet($ns)); + } else { + $keys = array(); + } + + return new KeyListIterator($this, $keys); + } + + /* FlushableInterface */ + + /** + * Flush the whole session container + * + * @return bool + */ + public function flush() + { + $this->getSessionContainer()->exchangeArray(array()); + return true; + } + + /* ClearByPrefixInterface */ + + /** + * Remove items matching given prefix + * + * @param string $prefix + * @return bool + */ + public function clearByPrefix($prefix) + { + $prefix = (string) $prefix; + if ($prefix === '') { + throw new Exception\InvalidArgumentException('No prefix given'); + } + + $cntr = $this->getSessionContainer(); + $ns = $this->getOptions()->getNamespace(); + + if (!$cntr->offsetExists($ns)) { + return true; + } + + $data = $cntr->offsetGet($ns); + $prefixL = strlen($prefix); + foreach ($data as $key => & $item) { + if (substr($key, 0, $prefixL) === $prefix) { + unset($data[$key]); + } + } + $cntr->offsetSet($ns, $data); + + return true; + } + + /* reading */ + + /** + * Internal method to get an item. + * + * @param string $normalizedKey + * @param bool $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) + { + $cntr = $this->getSessionContainer(); + $ns = $this->getOptions()->getNamespace(); + + if (!$cntr->offsetExists($ns)) { + $success = false; + return null; + } + + $data = $cntr->offsetGet($ns); + $success = array_key_exists($normalizedKey, $data); + if (!$success) { + return null; + } + + $casToken = $value = $data[$normalizedKey]; + return $value; + } + + /** + * Internal method to get multiple items. + * + * @param array $normalizedKeys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + */ + protected function internalGetItems(array & $normalizedKeys) + { + $cntr = $this->getSessionContainer(); + $ns = $this->getOptions()->getNamespace(); + + if (!$cntr->offsetExists($ns)) { + return array(); + } + + $data = $cntr->offsetGet($ns); + $result = array(); + foreach ($normalizedKeys as $normalizedKey) { + if (array_key_exists($normalizedKey, $data)) { + $result[$normalizedKey] = $data[$normalizedKey]; + } + } + + return $result; + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @return bool + */ + protected function internalHasItem(& $normalizedKey) + { + $cntr = $this->getSessionContainer(); + $ns = $this->getOptions()->getNamespace(); + + if (!$cntr->offsetExists($ns)) { + return false; + } + + $data = $cntr->offsetGet($ns); + return array_key_exists($normalizedKey, $data); + } + + /** + * Internal method to test multiple items. + * + * @param array $normalizedKeys + * @return array Array of found keys + */ + protected function internalHasItems(array & $normalizedKeys) + { + $cntr = $this->getSessionContainer(); + $ns = $this->getOptions()->getNamespace(); + + if (!$cntr->offsetExists($ns)) { + return array(); + } + + $data = $cntr->offsetGet($ns); + $result = array(); + foreach ($normalizedKeys as $normalizedKey) { + if (array_key_exists($normalizedKey, $data)) { + $result[] = $normalizedKey; + } + } + + return $result; + } + + /** + * Get metadata of an item. + * + * @param string $normalizedKey + * @return array|bool Metadata on success, false on failure + * @throws Exception\ExceptionInterface + * + * @triggers getMetadata.pre(PreEvent) + * @triggers getMetadata.post(PostEvent) + * @triggers getMetadata.exception(ExceptionEvent) + */ + protected function internalGetMetadata(& $normalizedKey) + { + return $this->internalHasItem($normalizedKey) ? array() : false; + } + + /* writing */ + + /** + * Internal method to store an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $cntr = $this->getSessionContainer(); + $ns = $this->getOptions()->getNamespace(); + $data = $cntr->offsetExists($ns) ? $cntr->offsetGet($ns) : array(); + $data[$normalizedKey] = $value; + $cntr->offsetSet($ns, $data); + return true; + } + + /** + * Internal method to store multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalSetItems(array & $normalizedKeyValuePairs) + { + $cntr = $this->getSessionContainer(); + $ns = $this->getOptions()->getNamespace(); + + if ($cntr->offsetExists($ns)) { + $data = array_merge($cntr->offsetGet($ns), $normalizedKeyValuePairs); + } else { + $data = $normalizedKeyValuePairs; + } + $cntr->offsetSet($ns, $data); + + return array(); + } + + /** + * Add an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalAddItem(& $normalizedKey, & $value) + { + $cntr = $this->getSessionContainer(); + $ns = $this->getOptions()->getNamespace(); + + if ($cntr->offsetExists($ns)) { + $data = $cntr->offsetGet($ns); + + if (array_key_exists($normalizedKey, $data)) { + return false; + } + + $data[$normalizedKey] = $value; + } else { + $data = array($normalizedKey => $value); + } + + $cntr->offsetSet($ns, $data); + return true; + } + + /** + * Internal method to add multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalAddItems(array & $normalizedKeyValuePairs) + { + $cntr = $this->getSessionContainer(); + $ns = $this->getOptions()->getNamespace(); + + $result = array(); + if ($cntr->offsetExists($ns)) { + $data = $cntr->offsetGet($ns); + + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + if (array_key_exists($normalizedKey, $data)) { + $result[] = $normalizedKey; + } else { + $data[$normalizedKey] = $value; + } + } + } else { + $data = $normalizedKeyValuePairs; + } + + $cntr->offsetSet($ns, $data); + return $result; + } + + /** + * Internal method to replace an existing item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalReplaceItem(& $normalizedKey, & $value) + { + $cntr = $this->getSessionContainer(); + $ns = $this->getOptions()->getNamespace(); + + if (!$cntr->offsetExists($ns)) { + return false; + } + + $data = $cntr->offsetGet($ns); + if (!array_key_exists($normalizedKey, $data)) { + return false; + } + $data[$normalizedKey] = $value; + $cntr->offsetSet($ns, $data); + + return true; + } + + /** + * Internal method to replace multiple existing items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalReplaceItems(array & $normalizedKeyValuePairs) + { + $cntr = $this->getSessionContainer(); + $ns = $this->getOptions()->getNamespace(); + if (!$cntr->offsetExists($ns)) { + return array_keys($normalizedKeyValuePairs); + } + + $data = $cntr->offsetGet($ns); + $result = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + if (!array_key_exists($normalizedKey, $data)) { + $result[] = $normalizedKey; + } else { + $data[$normalizedKey] = $value; + } + } + $cntr->offsetSet($ns, $data); + + return $result; + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItem(& $normalizedKey) + { + $cntr = $this->getSessionContainer(); + $ns = $this->getOptions()->getNamespace(); + + if (!$cntr->offsetExists($ns)) { + return false; + } + + $data = $cntr->offsetGet($ns); + if (!array_key_exists($normalizedKey, $data)) { + return false; + } + + unset($data[$normalizedKey]); + + if (!$data) { + $cntr->offsetUnset($ns); + } else { + $cntr->offsetSet($ns, $data); + } + + return true; + } + + /** + * Internal method to increment an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalIncrementItem(& $normalizedKey, & $value) + { + $cntr = $this->getSessionContainer(); + $ns = $this->getOptions()->getNamespace(); + + if ($cntr->offsetExists($ns)) { + $data = $cntr->offsetGet($ns); + } else { + $data = array(); + } + + if (array_key_exists($normalizedKey, $data)) { + $data[$normalizedKey]+= $value; + $newValue = $data[$normalizedKey]; + } else { + // initial value + $newValue = $value; + $data[$normalizedKey] = $newValue; + } + + $cntr->offsetSet($ns, $data); + return $newValue; + } + + /** + * Internal method to decrement an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalDecrementItem(& $normalizedKey, & $value) + { + $cntr = $this->getSessionContainer(); + $ns = $this->getOptions()->getNamespace(); + + if ($cntr->offsetExists($ns)) { + $data = $cntr->offsetGet($ns); + } else { + $data = array(); + } + + if (array_key_exists($normalizedKey, $data)) { + $data[$normalizedKey]-= $value; + $newValue = $data[$normalizedKey]; + } else { + // initial value + $newValue = -$value; + $data[$normalizedKey] = $newValue; + } + + $cntr->offsetSet($ns, $data); + return $newValue; + } + + /* status */ + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities === null) { + $this->capabilityMarker = new stdClass(); + $this->capabilities = new Capabilities( + $this, + $this->capabilityMarker, + array( + 'supportedDatatypes' => array( + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => 'array', + 'object' => 'object', + 'resource' => false, + ), + 'supportedMetadata' => array(), + 'minTtl' => 0, + 'maxKeyLength' => 0, + 'namespaceIsPrefix' => false, + 'namespaceSeparator' => '', + ) + ); + } + + return $this->capabilities; + } +} diff --git a/library/Zend/Cache/Storage/Adapter/SessionOptions.php b/library/Zend/Cache/Storage/Adapter/SessionOptions.php new file mode 100755 index 0000000000..8e4c6d5c90 --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/SessionOptions.php @@ -0,0 +1,51 @@ +sessionContainer != $sessionContainer) { + $this->triggerOptionEvent('session_container', $sessionContainer); + $this->sessionContainer = $sessionContainer; + } + + return $this; + } + + /** + * Get the session container + * + * @return null|SessionContainer + */ + public function getSessionContainer() + { + return $this->sessionContainer; + } +} diff --git a/library/Zend/Cache/Storage/Adapter/WinCache.php b/library/Zend/Cache/Storage/Adapter/WinCache.php new file mode 100755 index 0000000000..b911e04000 --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/WinCache.php @@ -0,0 +1,533 @@ +options) { + $this->setOptions(new WinCacheOptions()); + } + return $this->options; + } + + /* TotalSpaceCapableInterface */ + + /** + * Get total space in bytes + * + * @return int|float + */ + public function getTotalSpace() + { + $mem = wincache_ucache_meminfo(); + return $mem['memory_total']; + } + + /* AvailableSpaceCapableInterface */ + + /** + * Get available space in bytes + * + * @return int|float + */ + public function getAvailableSpace() + { + $mem = wincache_ucache_meminfo(); + return $mem['memory_free']; + } + + /* FlushableInterface */ + + /** + * Flush the whole storage + * + * @return bool + */ + public function flush() + { + return wincache_ucache_clear(); + } + + /* reading */ + + /** + * Internal method to get an item. + * + * @param string $normalizedKey + * @param bool $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $result = wincache_ucache_get($internalKey, $success); + + if ($success) { + $casToken = $result; + } else { + $result = null; + } + + return $result; + } + + /** + * Internal method to get multiple items. + * + * @param array $normalizedKeys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + */ + protected function internalGetItems(array & $normalizedKeys) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + if ($namespace === '') { + return wincache_ucache_get($normalizedKeys); + } + + $prefix = $namespace . $options->getNamespaceSeparator(); + $internalKeys = array(); + foreach ($normalizedKeys as $normalizedKey) { + $internalKeys[] = $prefix . $normalizedKey; + } + + $fetch = wincache_ucache_get($internalKeys); + + // remove namespace prefix + $prefixL = strlen($prefix); + $result = array(); + foreach ($fetch as $internalKey => & $value) { + $result[substr($internalKey, $prefixL)] = & $value; + } + + return $result; + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalHasItem(& $normalizedKey) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + return wincache_ucache_exists($prefix . $normalizedKey); + } + + /** + * Get metadata of an item. + * + * @param string $normalizedKey + * @return array|bool Metadata on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetMetadata(& $normalizedKey) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + + $info = wincache_ucache_info(true, $internalKey); + if (isset($info['ucache_entries'][1])) { + $metadata = $info['ucache_entries'][1]; + $this->normalizeMetadata($metadata); + return $metadata; + } + + return false; + } + + /* writing */ + + /** + * Internal method to store an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $ttl = $options->getTtl(); + + if (!wincache_ucache_set($internalKey, $value, $ttl)) { + $type = is_object($value) ? get_class($value) : gettype($value); + throw new Exception\RuntimeException( + "wincache_ucache_set('{$internalKey}', <{$type}>, {$ttl}) failed" + ); + } + + return true; + } + + /** + * Internal method to store multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalSetItems(array & $normalizedKeyValuePairs) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + if ($namespace === '') { + return wincache_ucache_set($normalizedKeyValuePairs, null, $options->getTtl()); + } + + $prefix = $namespace . $options->getNamespaceSeparator(); + $internalKeyValuePairs = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => & $value) { + $internalKey = $prefix . $normalizedKey; + $internalKeyValuePairs[$internalKey] = & $value; + } + + $result = wincache_ucache_set($internalKeyValuePairs, null, $options->getTtl()); + + // remove key prefic + $prefixL = strlen($prefix); + foreach ($result as & $key) { + $key = substr($key, $prefixL); + } + + return $result; + } + + /** + * Add an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalAddItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $ttl = $options->getTtl(); + + if (!wincache_ucache_add($internalKey, $value, $ttl)) { + $type = is_object($value) ? get_class($value) : gettype($value); + throw new Exception\RuntimeException( + "wincache_ucache_add('{$internalKey}', <{$type}>, {$ttl}) failed" + ); + } + + return true; + } + + /** + * Internal method to add multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalAddItems(array & $normalizedKeyValuePairs) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + if ($namespace === '') { + return wincache_ucache_add($normalizedKeyValuePairs, null, $options->getTtl()); + } + + $prefix = $namespace . $options->getNamespaceSeparator(); + $internalKeyValuePairs = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + $internalKey = $prefix . $normalizedKey; + $internalKeyValuePairs[$internalKey] = $value; + } + + $result = wincache_ucache_add($internalKeyValuePairs, null, $options->getTtl()); + + // remove key prefic + $prefixL = strlen($prefix); + foreach ($result as & $key) { + $key = substr($key, $prefixL); + } + + return $result; + } + + /** + * Internal method to replace an existing item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalReplaceItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + if (!wincache_ucache_exists($internalKey)) { + return false; + } + + $ttl = $options->getTtl(); + if (!wincache_ucache_set($internalKey, $value, $ttl)) { + $type = is_object($value) ? get_class($value) : gettype($value); + throw new Exception\RuntimeException( + "wincache_ucache_set('{$internalKey}', <{$type}>, {$ttl}) failed" + ); + } + + return true; + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItem(& $normalizedKey) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + return wincache_ucache_delete($internalKey); + } + + /** + * Internal method to remove multiple items. + * + * @param array $normalizedKeys + * @return array Array of not removed keys + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItems(array & $normalizedKeys) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + if ($namespace === '') { + $result = wincache_ucache_delete($normalizedKeys); + return ($result === false) ? $normalizedKeys : $result; + } + + $prefix = $namespace . $options->getNamespaceSeparator(); + $internalKeys = array(); + foreach ($normalizedKeys as $normalizedKey) { + $internalKeys[] = $prefix . $normalizedKey; + } + + $result = wincache_ucache_delete($internalKeys); + if ($result === false) { + return $normalizedKeys; + } elseif ($result) { + // remove key prefix + $prefixL = strlen($prefix); + foreach ($result as & $key) { + $key = substr($key, $prefixL); + } + } + + return $result; + } + + /** + * Internal method to increment an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalIncrementItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + return wincache_ucache_inc($internalKey, (int) $value); + } + + /** + * Internal method to decrement an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalDecrementItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + return wincache_ucache_dec($internalKey, (int) $value); + } + + /* status */ + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities === null) { + $marker = new stdClass(); + $capabilities = new Capabilities( + $this, + $marker, + array( + 'supportedDatatypes' => array( + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => true, + 'object' => 'object', + 'resource' => false, + ), + 'supportedMetadata' => array( + 'internal_key', 'ttl', 'hits', 'size' + ), + 'minTtl' => 1, + 'maxTtl' => 0, + 'staticTtl' => true, + 'ttlPrecision' => 1, + 'useRequestTime' => false, + 'expiredRead' => false, + 'namespaceIsPrefix' => true, + 'namespaceSeparator' => $this->getOptions()->getNamespaceSeparator(), + ) + ); + + // update namespace separator on change option + $this->getEventManager()->attach('option', function ($event) use ($capabilities, $marker) { + $params = $event->getParams(); + + if (isset($params['namespace_separator'])) { + $capabilities->setNamespaceSeparator($marker, $params['namespace_separator']); + } + }); + + $this->capabilities = $capabilities; + $this->capabilityMarker = $marker; + } + + return $this->capabilities; + } + + /* internal */ + + /** + * Normalize metadata to work with WinCache + * + * @param array $metadata + * @return void + */ + protected function normalizeMetadata(array & $metadata) + { + $metadata['internal_key'] = $metadata['key_name']; + $metadata['hits'] = $metadata['hitcount']; + $metadata['ttl'] = $metadata['ttl_seconds']; + $metadata['size'] = $metadata['value_size']; + + unset( + $metadata['key_name'], + $metadata['hitcount'], + $metadata['ttl_seconds'], + $metadata['value_size'] + ); + } +} diff --git a/library/Zend/Cache/Storage/Adapter/WinCacheOptions.php b/library/Zend/Cache/Storage/Adapter/WinCacheOptions.php new file mode 100755 index 0000000000..8c9789ae76 --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/WinCacheOptions.php @@ -0,0 +1,47 @@ +triggerOptionEvent('namespace_separator', $namespaceSeparator); + $this->namespaceSeparator = $namespaceSeparator; + return $this; + } + + /** + * Get namespace separator + * + * @return string + */ + public function getNamespaceSeparator() + { + return $this->namespaceSeparator; + } +} diff --git a/library/Zend/Cache/Storage/Adapter/XCache.php b/library/Zend/Cache/Storage/Adapter/XCache.php new file mode 100755 index 0000000000..18073e9fed --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/XCache.php @@ -0,0 +1,524 @@ +options) { + $this->setOptions(new XCacheOptions()); + } + return $this->options; + } + + /* TotalSpaceCapableInterface */ + + /** + * Get total space in bytes + * + * @return int|float + */ + public function getTotalSpace() + { + if ($this->totalSpace === null) { + $this->totalSpace = 0; + + $this->initAdminAuth(); + $cnt = xcache_count(XC_TYPE_VAR); + for ($i=0; $i < $cnt; $i++) { + $info = xcache_info(XC_TYPE_VAR, $i); + $this->totalSpace+= $info['size']; + } + $this->resetAdminAuth(); + } + + return $this->totalSpace; + } + + /* AvailableSpaceCapableInterface */ + + /** + * Get available space in bytes + * + * @return int|float + */ + public function getAvailableSpace() + { + $availableSpace = 0; + + $this->initAdminAuth(); + $cnt = xcache_count(XC_TYPE_VAR); + for ($i = 0; $i < $cnt; $i++) { + $info = xcache_info(XC_TYPE_VAR, $i); + $availableSpace+= $info['avail']; + } + $this->resetAdminAuth(); + + return $availableSpace; + } + + /* ClearByNamespaceInterface */ + + /** + * Remove items by given namespace + * + * @param string $namespace + * @return bool + */ + public function clearByNamespace($namespace) + { + $namespace = (string) $namespace; + if ($namespace === '') { + throw new Exception\InvalidArgumentException('No namespace given'); + } + + $options = $this->getOptions(); + $prefix = $namespace . $options->getNamespaceSeparator(); + + xcache_unset_by_prefix($prefix); + return true; + } + + /* ClearByPrefixInterface */ + + /** + * Remove items matching given prefix + * + * @param string $prefix + * @return bool + */ + public function clearByPrefix($prefix) + { + $prefix = (string) $prefix; + if ($prefix === '') { + throw new Exception\InvalidArgumentException('No prefix given'); + } + + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator() . $prefix; + + xcache_unset_by_prefix($prefix); + return true; + } + + /* FlushableInterface */ + + /** + * Flush the whole storage + * + * @return bool + */ + public function flush() + { + $this->initAdminAuth(); + $cnt = xcache_count(XC_TYPE_VAR); + for ($i = 0; $i < $cnt; $i++) { + xcache_clear_cache(XC_TYPE_VAR, $i); + } + $this->resetAdminAuth(); + + return true; + } + + /* IterableInterface */ + + /** + * Get the storage iterator + * + * @return KeyListIterator + */ + public function getIterator() + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $keys = array(); + + $this->initAdminAuth(); + + if ($namespace === '') { + $cnt = xcache_count(XC_TYPE_VAR); + for ($i=0; $i < $cnt; $i++) { + $list = xcache_list(XC_TYPE_VAR, $i); + foreach ($list['cache_list'] as & $item) { + $keys[] = $item['name']; + } + } + } else { + $prefix = $namespace . $options->getNamespaceSeparator(); + $prefixL = strlen($prefix); + + $cnt = xcache_count(XC_TYPE_VAR); + for ($i=0; $i < $cnt; $i++) { + $list = xcache_list(XC_TYPE_VAR, $i); + foreach ($list['cache_list'] as & $item) { + $keys[] = substr($item['name'], $prefixL); + } + } + } + + $this->resetAdminAuth(); + + return new KeyListIterator($this, $keys); + } + + /* reading */ + + /** + * Internal method to get an item. + * + * @param string $normalizedKey + * @param bool $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + + $result = xcache_get($internalKey); + $success = ($result !== null); + + if ($success) { + $casToken = $result; + } + + return $result; + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalHasItem(& $normalizedKey) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + return xcache_isset($prefix . $normalizedKey); + } + + /** + * Get metadata of an item. + * + * @param string $normalizedKey + * @return array|bool Metadata on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetMetadata(& $normalizedKey) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + + if (xcache_isset($internalKey)) { + $this->initAdminAuth(); + $cnt = xcache_count(XC_TYPE_VAR); + for ($i=0; $i < $cnt; $i++) { + $list = xcache_list(XC_TYPE_VAR, $i); + foreach ($list['cache_list'] as & $metadata) { + if ($metadata['name'] === $internalKey) { + $this->normalizeMetadata($metadata); + return $metadata; + } + } + } + $this->resetAdminAuth(); + } + + return false; + } + + /* writing */ + + /** + * Internal method to store an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($options === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $ttl = $options->getTtl(); + + if (!xcache_set($internalKey, $value, $ttl)) { + $type = is_object($value) ? get_class($value) : gettype($value); + throw new Exception\RuntimeException( + "xcache_set('{$internalKey}', <{$type}>, {$ttl}) failed" + ); + } + + return true; + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItem(& $normalizedKey) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + + return xcache_unset($internalKey); + } + + /** + * Internal method to increment an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalIncrementItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $ttl = $options->getTtl(); + $value = (int) $value; + + return xcache_inc($internalKey, $value, $ttl); + } + + /** + * Internal method to decrement an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalDecrementItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $ttl = $options->getTtl(); + $value = (int) $value; + + return xcache_dec($internalKey, $value, $ttl); + } + + /* status */ + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities === null) { + $marker = new stdClass(); + $capabilities = new Capabilities( + $this, + $marker, + array( + 'supportedDatatypes' => array( + 'NULL' => false, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => true, + 'object' => 'object', + 'resource' => false, + ), + 'supportedMetadata' => array( + 'internal_key', + 'size', 'refcount', 'hits', + 'ctime', 'atime', 'hvalue', + ), + 'minTtl' => 1, + 'maxTtl' => (int)ini_get('xcache.var_maxttl'), + 'staticTtl' => true, + 'ttlPrecision' => 1, + 'useRequestTime' => true, + 'expiredRead' => false, + 'maxKeyLength' => 5182, + 'namespaceIsPrefix' => true, + 'namespaceSeparator' => $this->getOptions()->getNamespaceSeparator(), + ) + ); + + // update namespace separator on change option + $this->getEventManager()->attach('option', function ($event) use ($capabilities, $marker) { + $params = $event->getParams(); + + if (isset($params['namespace_separator'])) { + $capabilities->setNamespaceSeparator($marker, $params['namespace_separator']); + } + }); + + $this->capabilities = $capabilities; + $this->capabilityMarker = $marker; + } + + return $this->capabilities; + } + + /* internal */ + + /** + * Init authentication before calling admin functions + * + * @return void + */ + protected function initAdminAuth() + { + $options = $this->getOptions(); + + if ($options->getAdminAuth()) { + $adminUser = $options->getAdminUser(); + $adminPass = $options->getAdminPass(); + + // backup HTTP authentication properties + if (isset($_SERVER['PHP_AUTH_USER'])) { + $this->backupAuth['PHP_AUTH_USER'] = $_SERVER['PHP_AUTH_USER']; + } + if (isset($_SERVER['PHP_AUTH_PW'])) { + $this->backupAuth['PHP_AUTH_PW'] = $_SERVER['PHP_AUTH_PW']; + } + + // set authentication + $_SERVER['PHP_AUTH_USER'] = $adminUser; + $_SERVER['PHP_AUTH_PW'] = $adminPass; + } + } + + /** + * Reset authentication after calling admin functions + * + * @return void + */ + protected function resetAdminAuth() + { + unset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']); + $_SERVER = $this->backupAuth + $_SERVER; + $this->backupAuth = array(); + } + + /** + * Normalize metadata to work with XCache + * + * @param array $metadata + */ + protected function normalizeMetadata(array & $metadata) + { + $metadata['internal_key'] = &$metadata['name']; + unset($metadata['name']); + } +} diff --git a/library/Zend/Cache/Storage/Adapter/XCacheOptions.php b/library/Zend/Cache/Storage/Adapter/XCacheOptions.php new file mode 100755 index 0000000000..4c97136b75 --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/XCacheOptions.php @@ -0,0 +1,146 @@ +triggerOptionEvent('namespace_separator', $namespaceSeparator); + $this->namespaceSeparator = $namespaceSeparator; + return $this; + } + + /** + * Get namespace separator + * + * @return string + */ + public function getNamespaceSeparator() + { + return $this->namespaceSeparator; + } + + /** + * Set username to call admin functions + * + * @param null|string $adminUser + * @return XCacheOptions + */ + public function setAdminUser($adminUser) + { + $adminUser = ($adminUser === null) ? null : (string) $adminUser; + if ($this->adminUser !== $adminUser) { + $this->triggerOptionEvent('admin_user', $adminUser); + $this->adminUser = $adminUser; + } + return $this; + } + + /** + * Get username to call admin functions + * + * @return string + */ + public function getAdminUser() + { + return $this->adminUser; + } + + /** + * Enable/Disable admin authentication handling + * + * @param bool $adminAuth + * @return XCacheOptions + */ + public function setAdminAuth($adminAuth) + { + $adminAuth = (bool) $adminAuth; + if ($this->adminAuth !== $adminAuth) { + $this->triggerOptionEvent('admin_auth', $adminAuth); + $this->adminAuth = $adminAuth; + } + return $this; + } + + /** + * Get admin authentication enabled + * + * @return bool + */ + public function getAdminAuth() + { + return $this->adminAuth; + } + + /** + * Set password to call admin functions + * + * @param null|string $adminPass + * @return XCacheOptions + */ + public function setAdminPass($adminPass) + { + $adminPass = ($adminPass === null) ? null : (string) $adminPass; + if ($this->adminPass !== $adminPass) { + $this->triggerOptionEvent('admin_pass', $adminPass); + $this->adminPass = $adminPass; + } + return $this; + } + + /** + * Get password to call admin functions + * + * @return string + */ + public function getAdminPass() + { + return $this->adminPass; + } +} diff --git a/library/Zend/Cache/Storage/Adapter/ZendServerDisk.php b/library/Zend/Cache/Storage/Adapter/ZendServerDisk.php new file mode 100755 index 0000000000..c48992a59b --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/ZendServerDisk.php @@ -0,0 +1,186 @@ +totalSpace === null) { + $path = ini_get('zend_datacache.disk.save_path'); + + ErrorHandler::start(); + $total = disk_total_space($path); + $error = ErrorHandler::stop(); + if ($total === false) { + throw new Exception\RuntimeException("Can't detect total space of '{$path}'", 0, $error); + } + + $this->totalSpace = $total; + } + return $this->totalSpace; + } + + /* AvailableSpaceCapableInterface */ + + /** + * Get available space in bytes + * + * @throws Exception\RuntimeException + * @return int|float + */ + public function getAvailableSpace() + { + $path = ini_get('zend_datacache.disk.save_path'); + + ErrorHandler::start(); + $avail = disk_free_space($path); + $error = ErrorHandler::stop(); + if ($avail === false) { + throw new Exception\RuntimeException("Can't detect free space of '{$path}'", 0, $error); + } + + return $avail; + } + + /* internal */ + + /** + * Store data into Zend Data Disk Cache + * + * @param string $internalKey + * @param mixed $value + * @param int $ttl + * @return void + * @throws Exception\RuntimeException + */ + protected function zdcStore($internalKey, $value, $ttl) + { + if (!zend_disk_cache_store($internalKey, $value, $ttl)) { + $valueType = gettype($value); + throw new Exception\RuntimeException( + "zend_disk_cache_store($internalKey, <{$valueType}>, {$ttl}) failed" + ); + } + } + + /** + * Fetch a single item from Zend Data Disk Cache + * + * @param string $internalKey + * @return mixed The stored value or NULL if item wasn't found + * @throws Exception\RuntimeException + */ + protected function zdcFetch($internalKey) + { + return zend_disk_cache_fetch((string) $internalKey); + } + + /** + * Fetch multiple items from Zend Data Disk Cache + * + * @param array $internalKeys + * @return array All found items + * @throws Exception\RuntimeException + */ + protected function zdcFetchMulti(array $internalKeys) + { + $items = zend_disk_cache_fetch($internalKeys); + if ($items === false) { + throw new Exception\RuntimeException("zend_disk_cache_fetch() failed"); + } + return $items; + } + + /** + * Delete data from Zend Data Disk Cache + * + * @param string $internalKey + * @return bool + * @throws Exception\RuntimeException + */ + protected function zdcDelete($internalKey) + { + return zend_disk_cache_delete($internalKey); + } +} diff --git a/library/Zend/Cache/Storage/Adapter/ZendServerShm.php b/library/Zend/Cache/Storage/Adapter/ZendServerShm.php new file mode 100755 index 0000000000..6b226d7c0e --- /dev/null +++ b/library/Zend/Cache/Storage/Adapter/ZendServerShm.php @@ -0,0 +1,141 @@ +, {$ttl}) failed" + ); + } + } + + /** + * Fetch a single item from Zend Data SHM Cache + * + * @param string $internalKey + * @return mixed The stored value or NULL if item wasn't found + * @throws Exception\RuntimeException + */ + protected function zdcFetch($internalKey) + { + return zend_shm_cache_fetch((string) $internalKey); + } + + /** + * Fetch multiple items from Zend Data SHM Cache + * + * @param array $internalKeys + * @return array All found items + * @throws Exception\RuntimeException + */ + protected function zdcFetchMulti(array $internalKeys) + { + $items = zend_shm_cache_fetch($internalKeys); + if ($items === false) { + throw new Exception\RuntimeException("zend_shm_cache_fetch() failed"); + } + return $items; + } + + /** + * Delete data from Zend Data SHM Cache + * + * @param string $internalKey + * @return bool + * @throws Exception\RuntimeException + */ + protected function zdcDelete($internalKey) + { + return zend_shm_cache_delete($internalKey); + } +} diff --git a/library/Zend/Cache/Storage/AdapterPluginManager.php b/library/Zend/Cache/Storage/AdapterPluginManager.php new file mode 100755 index 0000000000..665b36c813 --- /dev/null +++ b/library/Zend/Cache/Storage/AdapterPluginManager.php @@ -0,0 +1,74 @@ + 'Zend\Cache\Storage\Adapter\Apc', + 'blackhole' => 'Zend\Cache\Storage\Adapter\BlackHole', + 'dba' => 'Zend\Cache\Storage\Adapter\Dba', + 'filesystem' => 'Zend\Cache\Storage\Adapter\Filesystem', + 'memcache' => 'Zend\Cache\Storage\Adapter\Memcache', + 'memcached' => 'Zend\Cache\Storage\Adapter\Memcached', + 'memory' => 'Zend\Cache\Storage\Adapter\Memory', + 'redis' => 'Zend\Cache\Storage\Adapter\Redis', + 'session' => 'Zend\Cache\Storage\Adapter\Session', + 'xcache' => 'Zend\Cache\Storage\Adapter\XCache', + 'wincache' => 'Zend\Cache\Storage\Adapter\WinCache', + 'zendserverdisk' => 'Zend\Cache\Storage\Adapter\ZendServerDisk', + 'zendservershm' => 'Zend\Cache\Storage\Adapter\ZendServerShm', + ); + + /** + * Do not share by default + * + * @var array + */ + protected $shareByDefault = false; + + /** + * Validate the plugin + * + * Checks that the adapter loaded is an instance of StorageInterface. + * + * @param mixed $plugin + * @return void + * @throws Exception\RuntimeException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof StorageInterface) { + // we're okay + return; + } + + throw new Exception\RuntimeException(sprintf( + 'Plugin of type %s is invalid; must implement %s\StorageInterface', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/library/Zend/Cache/Storage/AvailableSpaceCapableInterface.php b/library/Zend/Cache/Storage/AvailableSpaceCapableInterface.php new file mode 100755 index 0000000000..5088fe255a --- /dev/null +++ b/library/Zend/Cache/Storage/AvailableSpaceCapableInterface.php @@ -0,0 +1,20 @@ +storage = $storage; + $this->marker = $marker; + $this->baseCapabilities = $baseCapabilities; + + foreach ($capabilities as $name => $value) { + $this->setCapability($marker, $name, $value); + } + } + + /** + * Get the storage adapter + * + * @return StorageInterface + */ + public function getAdapter() + { + return $this->storage; + } + + /** + * Get supported datatypes + * + * @return array + */ + public function getSupportedDatatypes() + { + return $this->getCapability('supportedDatatypes', array( + 'NULL' => false, + 'boolean' => false, + 'integer' => false, + 'double' => false, + 'string' => true, + 'array' => false, + 'object' => false, + 'resource' => false, + )); + } + + /** + * Set supported datatypes + * + * @param stdClass $marker + * @param array $datatypes + * @throws Exception\InvalidArgumentException + * @return Capabilities Fluent interface + */ + public function setSupportedDatatypes(stdClass $marker, array $datatypes) + { + $allTypes = array( + 'array', + 'boolean', + 'double', + 'integer', + 'NULL', + 'object', + 'resource', + 'string', + ); + + // check/normalize datatype values + foreach ($datatypes as $type => &$toType) { + if (!in_array($type, $allTypes)) { + throw new Exception\InvalidArgumentException("Unknown datatype '{$type}'"); + } + + if (is_string($toType)) { + $toType = strtolower($toType); + if (!in_array($toType, $allTypes)) { + throw new Exception\InvalidArgumentException("Unknown datatype '{$toType}'"); + } + } else { + $toType = (bool) $toType; + } + } + + // add missing datatypes as not supported + $missingTypes = array_diff($allTypes, array_keys($datatypes)); + foreach ($missingTypes as $type) { + $datatypes[$type] = false; + } + + return $this->setCapability($marker, 'supportedDatatypes', $datatypes); + } + + /** + * Get supported metadata + * + * @return array + */ + public function getSupportedMetadata() + { + return $this->getCapability('supportedMetadata', array()); + } + + /** + * Set supported metadata + * + * @param stdClass $marker + * @param string[] $metadata + * @throws Exception\InvalidArgumentException + * @return Capabilities Fluent interface + */ + public function setSupportedMetadata(stdClass $marker, array $metadata) + { + foreach ($metadata as $name) { + if (!is_string($name)) { + throw new Exception\InvalidArgumentException('$metadata must be an array of strings'); + } + } + return $this->setCapability($marker, 'supportedMetadata', $metadata); + } + + /** + * Get minimum supported time-to-live + * + * @return int 0 means items never expire + */ + public function getMinTtl() + { + return $this->getCapability('minTtl', 0); + } + + /** + * Set minimum supported time-to-live + * + * @param stdClass $marker + * @param int $minTtl + * @throws Exception\InvalidArgumentException + * @return Capabilities Fluent interface + */ + public function setMinTtl(stdClass $marker, $minTtl) + { + $minTtl = (int) $minTtl; + if ($minTtl < 0) { + throw new Exception\InvalidArgumentException('$minTtl must be greater or equal 0'); + } + return $this->setCapability($marker, 'minTtl', $minTtl); + } + + /** + * Get maximum supported time-to-live + * + * @return int 0 means infinite + */ + public function getMaxTtl() + { + return $this->getCapability('maxTtl', 0); + } + + /** + * Set maximum supported time-to-live + * + * @param stdClass $marker + * @param int $maxTtl + * @throws Exception\InvalidArgumentException + * @return Capabilities Fluent interface + */ + public function setMaxTtl(stdClass $marker, $maxTtl) + { + $maxTtl = (int) $maxTtl; + if ($maxTtl < 0) { + throw new Exception\InvalidArgumentException('$maxTtl must be greater or equal 0'); + } + return $this->setCapability($marker, 'maxTtl', $maxTtl); + } + + /** + * Is the time-to-live handled static (on write) + * or dynamic (on read) + * + * @return bool + */ + public function getStaticTtl() + { + return $this->getCapability('staticTtl', false); + } + + /** + * Set if the time-to-live handled static (on write) or dynamic (on read) + * + * @param stdClass $marker + * @param bool $flag + * @return Capabilities Fluent interface + */ + public function setStaticTtl(stdClass $marker, $flag) + { + return $this->setCapability($marker, 'staticTtl', (bool) $flag); + } + + /** + * Get time-to-live precision + * + * @return float + */ + public function getTtlPrecision() + { + return $this->getCapability('ttlPrecision', 1); + } + + /** + * Set time-to-live precision + * + * @param stdClass $marker + * @param float $ttlPrecision + * @throws Exception\InvalidArgumentException + * @return Capabilities Fluent interface + */ + public function setTtlPrecision(stdClass $marker, $ttlPrecision) + { + $ttlPrecision = (float) $ttlPrecision; + if ($ttlPrecision <= 0) { + throw new Exception\InvalidArgumentException('$ttlPrecision must be greater than 0'); + } + return $this->setCapability($marker, 'ttlPrecision', $ttlPrecision); + } + + /** + * Get use request time + * + * @return bool + */ + public function getUseRequestTime() + { + return $this->getCapability('useRequestTime', false); + } + + /** + * Set use request time + * + * @param stdClass $marker + * @param bool $flag + * @return Capabilities Fluent interface + */ + public function setUseRequestTime(stdClass $marker, $flag) + { + return $this->setCapability($marker, 'useRequestTime', (bool) $flag); + } + + /** + * Get if expired items are readable + * + * @return bool + */ + public function getExpiredRead() + { + return $this->getCapability('expiredRead', false); + } + + /** + * Set if expired items are readable + * + * @param stdClass $marker + * @param bool $flag + * @return Capabilities Fluent interface + */ + public function setExpiredRead(stdClass $marker, $flag) + { + return $this->setCapability($marker, 'expiredRead', (bool) $flag); + } + + /** + * Get maximum key lenth + * + * @return int -1 means unknown, 0 means infinite + */ + public function getMaxKeyLength() + { + return $this->getCapability('maxKeyLength', -1); + } + + /** + * Set maximum key length + * + * @param stdClass $marker + * @param int $maxKeyLength + * @throws Exception\InvalidArgumentException + * @return Capabilities Fluent interface + */ + public function setMaxKeyLength(stdClass $marker, $maxKeyLength) + { + $maxKeyLength = (int) $maxKeyLength; + if ($maxKeyLength < -1) { + throw new Exception\InvalidArgumentException('$maxKeyLength must be greater or equal than -1'); + } + return $this->setCapability($marker, 'maxKeyLength', $maxKeyLength); + } + + /** + * Get if namespace support is implemented as prefix + * + * @return bool + */ + public function getNamespaceIsPrefix() + { + return $this->getCapability('namespaceIsPrefix', true); + } + + /** + * Set if namespace support is implemented as prefix + * + * @param stdClass $marker + * @param bool $flag + * @return Capabilities Fluent interface + */ + public function setNamespaceIsPrefix(stdClass $marker, $flag) + { + return $this->setCapability($marker, 'namespaceIsPrefix', (bool) $flag); + } + + /** + * Get namespace separator if namespace is implemented as prefix + * + * @return string + */ + public function getNamespaceSeparator() + { + return $this->getCapability('namespaceSeparator', ''); + } + + /** + * Set the namespace separator if namespace is implemented as prefix + * + * @param stdClass $marker + * @param string $separator + * @return Capabilities Fluent interface + */ + public function setNamespaceSeparator(stdClass $marker, $separator) + { + return $this->setCapability($marker, 'namespaceSeparator', (string) $separator); + } + + /** + * Get a capability + * + * @param string $property + * @param mixed $default + * @return mixed + */ + protected function getCapability($property, $default = null) + { + if ($this->$property !== null) { + return $this->$property; + } elseif ($this->baseCapabilities) { + $getMethod = 'get' . $property; + return $this->baseCapabilities->$getMethod(); + } + return $default; + } + + /** + * Change a capability + * + * @param stdClass $marker + * @param string $property + * @param mixed $value + * @return Capabilities Fluent interface + * @throws Exception\InvalidArgumentException + */ + protected function setCapability(stdClass $marker, $property, $value) + { + if ($this->marker !== $marker) { + throw new Exception\InvalidArgumentException('Invalid marker'); + } + + if ($this->$property !== $value) { + $this->$property = $value; + + // trigger event + if ($this->storage instanceof EventsCapableInterface) { + $this->storage->getEventManager()->trigger('capability', $this->storage, new ArrayObject(array( + $property => $value + ))); + } + } + + return $this; + } +} diff --git a/library/Zend/Cache/Storage/ClearByNamespaceInterface.php b/library/Zend/Cache/Storage/ClearByNamespaceInterface.php new file mode 100755 index 0000000000..15fb4e7566 --- /dev/null +++ b/library/Zend/Cache/Storage/ClearByNamespaceInterface.php @@ -0,0 +1,21 @@ +setStorage($target); + } + + /** + * Alias of setTarget + * + * @param StorageInterface $storage + * @return Event + * @see Zend\EventManager\Event::setTarget() + */ + public function setStorage(StorageInterface $storage) + { + $this->target = $storage; + return $this; + } + + /** + * Alias of getTarget + * + * @return StorageInterface + */ + public function getStorage() + { + return $this->getTarget(); + } +} diff --git a/library/Zend/Cache/Storage/ExceptionEvent.php b/library/Zend/Cache/Storage/ExceptionEvent.php new file mode 100755 index 0000000000..e9ffb4a0c2 --- /dev/null +++ b/library/Zend/Cache/Storage/ExceptionEvent.php @@ -0,0 +1,91 @@ +setException($exception); + } + + /** + * Set the exception to be thrown + * + * @param Exception $exception + * @return ExceptionEvent + */ + public function setException(Exception $exception) + { + $this->exception = $exception; + return $this; + } + + /** + * Get the exception to be thrown + * + * @return Exception + */ + public function getException() + { + return $this->exception; + } + + /** + * Throw the exception or use the result + * + * @param bool $flag + * @return ExceptionEvent + */ + public function setThrowException($flag) + { + $this->throwException = (bool) $flag; + return $this; + } + + /** + * Throw the exception or use the result + * + * @return bool + */ + public function getThrowException() + { + return $this->throwException; + } +} diff --git a/library/Zend/Cache/Storage/FlushableInterface.php b/library/Zend/Cache/Storage/FlushableInterface.php new file mode 100755 index 0000000000..6c72541e6b --- /dev/null +++ b/library/Zend/Cache/Storage/FlushableInterface.php @@ -0,0 +1,20 @@ +options = $options; + return $this; + } + + /** + * Get all pattern options + * + * @return PluginOptions + */ + public function getOptions() + { + if (null === $this->options) { + $this->setOptions(new PluginOptions()); + } + return $this->options; + } +} diff --git a/library/Zend/Cache/Storage/Plugin/ClearExpiredByFactor.php b/library/Zend/Cache/Storage/Plugin/ClearExpiredByFactor.php new file mode 100755 index 0000000000..0b257be779 --- /dev/null +++ b/library/Zend/Cache/Storage/Plugin/ClearExpiredByFactor.php @@ -0,0 +1,49 @@ +listeners[] = $events->attach('setItem.post', $callback, $priority); + $this->listeners[] = $events->attach('setItems.post', $callback, $priority); + $this->listeners[] = $events->attach('addItem.post', $callback, $priority); + $this->listeners[] = $events->attach('addItems.post', $callback, $priority); + } + + /** + * Clear expired items by factor after writing new item(s) + * + * @param PostEvent $event + * @return void + */ + public function clearExpiredByFactor(PostEvent $event) + { + $storage = $event->getStorage(); + if (!($storage instanceof ClearExpiredInterface)) { + return; + } + + $factor = $this->getOptions()->getClearingFactor(); + if ($factor && mt_rand(1, $factor) == 1) { + $storage->clearExpired(); + } + } +} diff --git a/library/Zend/Cache/Storage/Plugin/ExceptionHandler.php b/library/Zend/Cache/Storage/Plugin/ExceptionHandler.php new file mode 100755 index 0000000000..e6acace0c3 --- /dev/null +++ b/library/Zend/Cache/Storage/Plugin/ExceptionHandler.php @@ -0,0 +1,79 @@ +listeners[] = $events->attach('getItem.exception', $callback, $priority); + $this->listeners[] = $events->attach('getItems.exception', $callback, $priority); + + $this->listeners[] = $events->attach('hasItem.exception', $callback, $priority); + $this->listeners[] = $events->attach('hasItems.exception', $callback, $priority); + + $this->listeners[] = $events->attach('getMetadata.exception', $callback, $priority); + $this->listeners[] = $events->attach('getMetadatas.exception', $callback, $priority); + + // write + $this->listeners[] = $events->attach('setItem.exception', $callback, $priority); + $this->listeners[] = $events->attach('setItems.exception', $callback, $priority); + + $this->listeners[] = $events->attach('addItem.exception', $callback, $priority); + $this->listeners[] = $events->attach('addItems.exception', $callback, $priority); + + $this->listeners[] = $events->attach('replaceItem.exception', $callback, $priority); + $this->listeners[] = $events->attach('replaceItems.exception', $callback, $priority); + + $this->listeners[] = $events->attach('touchItem.exception', $callback, $priority); + $this->listeners[] = $events->attach('touchItems.exception', $callback, $priority); + + $this->listeners[] = $events->attach('removeItem.exception', $callback, $priority); + $this->listeners[] = $events->attach('removeItems.exception', $callback, $priority); + + $this->listeners[] = $events->attach('checkAndSetItem.exception', $callback, $priority); + + // increment / decrement item(s) + $this->listeners[] = $events->attach('incrementItem.exception', $callback, $priority); + $this->listeners[] = $events->attach('incrementItems.exception', $callback, $priority); + + $this->listeners[] = $events->attach('decrementItem.exception', $callback, $priority); + $this->listeners[] = $events->attach('decrementItems.exception', $callback, $priority); + + // utility + $this->listeners[] = $events->attach('clearExpired.exception', $callback, $priority); + } + + /** + * On exception + * + * @param ExceptionEvent $event + * @return void + */ + public function onException(ExceptionEvent $event) + { + $options = $this->getOptions(); + $callback = $options->getExceptionCallback(); + if ($callback) { + call_user_func($callback, $event->getException()); + } + + $event->setThrowException($options->getThrowExceptions()); + } +} diff --git a/library/Zend/Cache/Storage/Plugin/IgnoreUserAbort.php b/library/Zend/Cache/Storage/Plugin/IgnoreUserAbort.php new file mode 100755 index 0000000000..ddec6354c6 --- /dev/null +++ b/library/Zend/Cache/Storage/Plugin/IgnoreUserAbort.php @@ -0,0 +1,117 @@ +listeners[] = $events->attach('setItem.pre', $cbOnBefore, $priority); + $this->listeners[] = $events->attach('setItem.post', $cbOnAfter, $priority); + $this->listeners[] = $events->attach('setItem.exception', $cbOnAfter, $priority); + + $this->listeners[] = $events->attach('setItems.pre', $cbOnBefore, $priority); + $this->listeners[] = $events->attach('setItems.post', $cbOnAfter, $priority); + $this->listeners[] = $events->attach('setItems.exception', $cbOnAfter, $priority); + + $this->listeners[] = $events->attach('addItem.pre', $cbOnBefore, $priority); + $this->listeners[] = $events->attach('addItem.post', $cbOnAfter, $priority); + $this->listeners[] = $events->attach('addItem.exception', $cbOnAfter, $priority); + + $this->listeners[] = $events->attach('addItems.pre', $cbOnBefore, $priority); + $this->listeners[] = $events->attach('addItems.post', $cbOnAfter, $priority); + $this->listeners[] = $events->attach('addItems.exception', $cbOnAfter, $priority); + + $this->listeners[] = $events->attach('replaceItem.pre', $cbOnBefore, $priority); + $this->listeners[] = $events->attach('replaceItem.post', $cbOnAfter, $priority); + $this->listeners[] = $events->attach('replaceItem.exception', $cbOnAfter, $priority); + + $this->listeners[] = $events->attach('replaceItems.pre', $cbOnBefore, $priority); + $this->listeners[] = $events->attach('replaceItems.post', $cbOnAfter, $priority); + $this->listeners[] = $events->attach('replaceItems.exception', $cbOnAfter, $priority); + + $this->listeners[] = $events->attach('checkAndSetItem.pre', $cbOnBefore, $priority); + $this->listeners[] = $events->attach('checkAndSetItem.post', $cbOnAfter, $priority); + $this->listeners[] = $events->attach('checkAndSetItem.exception', $cbOnAfter, $priority); + + // increment / decrement item(s) + $this->listeners[] = $events->attach('incrementItem.pre', $cbOnBefore, $priority); + $this->listeners[] = $events->attach('incrementItem.post', $cbOnAfter, $priority); + $this->listeners[] = $events->attach('incrementItem.exception', $cbOnAfter, $priority); + + $this->listeners[] = $events->attach('incrementItems.pre', $cbOnBefore, $priority); + $this->listeners[] = $events->attach('incrementItems.post', $cbOnAfter, $priority); + $this->listeners[] = $events->attach('incrementItems.exception', $cbOnAfter, $priority); + + $this->listeners[] = $events->attach('decrementItem.pre', $cbOnBefore, $priority); + $this->listeners[] = $events->attach('decrementItem.post', $cbOnAfter, $priority); + $this->listeners[] = $events->attach('decrementItem.exception', $cbOnAfter, $priority); + + $this->listeners[] = $events->attach('decrementItems.pre', $cbOnBefore, $priority); + $this->listeners[] = $events->attach('decrementItems.post', $cbOnAfter, $priority); + $this->listeners[] = $events->attach('decrementItems.exception', $cbOnAfter, $priority); + } + + /** + * Activate ignore_user_abort if not already done + * and save the target who activated it. + * + * @param Event $event + * @return void + */ + public function onBefore(Event $event) + { + if ($this->activatedTarget === null && !ignore_user_abort(true)) { + $this->activatedTarget = $event->getTarget(); + } + } + + /** + * Reset ignore_user_abort if it's activated and if it's the same target + * who activated it. + * + * If exit_on_abort is enabled and the connection has been aborted + * exit the script. + * + * @param Event $event + * @return void + */ + public function onAfter(Event $event) + { + if ($this->activatedTarget === $event->getTarget()) { + // exit if connection aborted + if ($this->getOptions()->getExitOnAbort() && connection_aborted()) { + exit; + } + + // reset ignore_user_abort + ignore_user_abort(false); + + // remove activated target + $this->activatedTarget = null; + } + } +} diff --git a/library/Zend/Cache/Storage/Plugin/OptimizeByFactor.php b/library/Zend/Cache/Storage/Plugin/OptimizeByFactor.php new file mode 100755 index 0000000000..42643df7a3 --- /dev/null +++ b/library/Zend/Cache/Storage/Plugin/OptimizeByFactor.php @@ -0,0 +1,46 @@ +listeners[] = $events->attach('removeItem.post', $callback, $priority); + $this->listeners[] = $events->attach('removeItems.post', $callback, $priority); + } + + /** + * Optimize by factor on a success _RESULT_ + * + * @param PostEvent $event + * @return void + */ + public function optimizeByFactor(PostEvent $event) + { + $storage = $event->getStorage(); + if (!($storage instanceof OptimizableInterface)) { + return; + } + + $factor = $this->getOptions()->getOptimizingFactor(); + if ($factor && mt_rand(1, $factor) == 1) { + $storage->optimize(); + } + } +} diff --git a/library/Zend/Cache/Storage/Plugin/PluginInterface.php b/library/Zend/Cache/Storage/Plugin/PluginInterface.php new file mode 100755 index 0000000000..af3d007ca4 --- /dev/null +++ b/library/Zend/Cache/Storage/Plugin/PluginInterface.php @@ -0,0 +1,30 @@ +clearingFactor = $this->normalizeFactor($clearingFactor); + return $this; + } + + /** + * Get automatic clearing factor + * + * Used by: + * - ClearExpiredByFactor + * + * @return int + */ + public function getClearingFactor() + { + return $this->clearingFactor; + } + + /** + * Set callback to call on intercepted exception + * + * Used by: + * - ExceptionHandler + * + * @param callable $exceptionCallback + * @throws Exception\InvalidArgumentException + * @return PluginOptions + */ + public function setExceptionCallback($exceptionCallback) + { + if ($exceptionCallback !== null && !is_callable($exceptionCallback, true)) { + throw new Exception\InvalidArgumentException('Not a valid callback'); + } + $this->exceptionCallback = $exceptionCallback; + return $this; + } + + /** + * Get callback to call on intercepted exception + * + * Used by: + * - ExceptionHandler + * + * @return callable + */ + public function getExceptionCallback() + { + return $this->exceptionCallback; + } + + /** + * Exit if connection aborted and ignore_user_abort is disabled. + * + * @param bool $exitOnAbort + * @return PluginOptions + */ + public function setExitOnAbort($exitOnAbort) + { + $this->exitOnAbort = (bool) $exitOnAbort; + return $this; + } + + /** + * Exit if connection aborted and ignore_user_abort is disabled. + * + * @return bool + */ + public function getExitOnAbort() + { + return $this->exitOnAbort; + } + + /** + * Set automatic optimizing factor + * + * Used by: + * - OptimizeByFactor + * + * @param int $optimizingFactor + * @return PluginOptions + */ + public function setOptimizingFactor($optimizingFactor) + { + $this->optimizingFactor = $this->normalizeFactor($optimizingFactor); + return $this; + } + + /** + * Set automatic optimizing factor + * + * Used by: + * - OptimizeByFactor + * + * @return int + */ + public function getOptimizingFactor() + { + return $this->optimizingFactor; + } + + /** + * Set serializer + * + * Used by: + * - Serializer + * + * @param string|SerializerAdapter $serializer + * @throws Exception\InvalidArgumentException + * @return self + */ + public function setSerializer($serializer) + { + if (!is_string($serializer) && !$serializer instanceof SerializerAdapter) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects either a string serializer name or Zend\Serializer\Adapter\AdapterInterface instance; ' + . 'received "%s"', + __METHOD__, + (is_object($serializer) ? get_class($serializer) : gettype($serializer)) + )); + } + $this->serializer = $serializer; + return $this; + } + + /** + * Get serializer + * + * Used by: + * - Serializer + * + * @return SerializerAdapter + */ + public function getSerializer() + { + if (!$this->serializer instanceof SerializerAdapter) { + // use default serializer + if (!$this->serializer) { + $this->setSerializer(SerializerFactory::getDefaultAdapter()); + // instantiate by class name + serializer_options + } else { + $options = $this->getSerializerOptions(); + $this->setSerializer(SerializerFactory::factory($this->serializer, $options)); + } + } + return $this->serializer; + } + + /** + * Set configuration options for instantiating a serializer adapter + * + * Used by: + * - Serializer + * + * @param mixed $serializerOptions + * @return PluginOptions + */ + public function setSerializerOptions($serializerOptions) + { + $this->serializerOptions = $serializerOptions; + return $this; + } + + /** + * Get configuration options for instantiating a serializer adapter + * + * Used by: + * - Serializer + * + * @return array + */ + public function getSerializerOptions() + { + return $this->serializerOptions; + } + + /** + * Set flag indicating we should re-throw exceptions + * + * Used by: + * - ExceptionHandler + * + * @param bool $throwExceptions + * @return PluginOptions + */ + public function setThrowExceptions($throwExceptions) + { + $this->throwExceptions = (bool) $throwExceptions; + return $this; + } + + /** + * Should we re-throw exceptions? + * + * Used by: + * - ExceptionHandler + * + * @return bool + */ + public function getThrowExceptions() + { + return $this->throwExceptions; + } + + /** + * Normalize a factor + * + * Cast to int and ensure we have a value greater than zero. + * + * @param int $factor + * @return int + * @throws Exception\InvalidArgumentException + */ + protected function normalizeFactor($factor) + { + $factor = (int) $factor; + if ($factor < 0) { + throw new Exception\InvalidArgumentException( + "Invalid factor '{$factor}': must be greater or equal 0" + ); + } + return $factor; + } +} diff --git a/library/Zend/Cache/Storage/Plugin/Serializer.php b/library/Zend/Cache/Storage/Plugin/Serializer.php new file mode 100755 index 0000000000..c785e751eb --- /dev/null +++ b/library/Zend/Cache/Storage/Plugin/Serializer.php @@ -0,0 +1,259 @@ +listeners[] = $events->attach('getItem.post', array($this, 'onReadItemPost'), $postPriority); + $this->listeners[] = $events->attach('getItems.post', array($this, 'onReadItemsPost'), $postPriority); + + // write + $this->listeners[] = $events->attach('setItem.pre', array($this, 'onWriteItemPre'), $prePriority); + $this->listeners[] = $events->attach('setItems.pre', array($this, 'onWriteItemsPre'), $prePriority); + + $this->listeners[] = $events->attach('addItem.pre', array($this, 'onWriteItemPre'), $prePriority); + $this->listeners[] = $events->attach('addItems.pre', array($this, 'onWriteItemsPre'), $prePriority); + + $this->listeners[] = $events->attach('replaceItem.pre', array($this, 'onWriteItemPre'), $prePriority); + $this->listeners[] = $events->attach('replaceItems.pre', array($this, 'onWriteItemsPre'), $prePriority); + + $this->listeners[] = $events->attach('checkAndSetItem.pre', array($this, 'onWriteItemPre'), $prePriority); + + // increment / decrement item(s) + $this->listeners[] = $events->attach('incrementItem.pre', array($this, 'onIncrementItemPre'), $prePriority); + $this->listeners[] = $events->attach('incrementItems.pre', array($this, 'onIncrementItemsPre'), $prePriority); + + $this->listeners[] = $events->attach('decrementItem.pre', array($this, 'onDecrementItemPre'), $prePriority); + $this->listeners[] = $events->attach('decrementItems.pre', array($this, 'onDecrementItemsPre'), $prePriority); + + // overwrite capabilities + $this->listeners[] = $events->attach('getCapabilities.post', array($this, 'onGetCapabilitiesPost'), $postPriority); + } + + /** + * On read item post + * + * @param PostEvent $event + * @return void + */ + public function onReadItemPost(PostEvent $event) + { + $serializer = $this->getOptions()->getSerializer(); + $result = $event->getResult(); + $result = $serializer->unserialize($result); + $event->setResult($result); + } + + /** + * On read items post + * + * @param PostEvent $event + * @return void + */ + public function onReadItemsPost(PostEvent $event) + { + $serializer = $this->getOptions()->getSerializer(); + $result = $event->getResult(); + foreach ($result as &$value) { + $value = $serializer->unserialize($value); + } + $event->setResult($result); + } + + /** + * On write item pre + * + * @param Event $event + * @return void + */ + public function onWriteItemPre(Event $event) + { + $serializer = $this->getOptions()->getSerializer(); + $params = $event->getParams(); + $params['value'] = $serializer->serialize($params['value']); + } + + /** + * On write items pre + * + * @param Event $event + * @return void + */ + public function onWriteItemsPre(Event $event) + { + $serializer = $this->getOptions()->getSerializer(); + $params = $event->getParams(); + foreach ($params['keyValuePairs'] as &$value) { + $value = $serializer->serialize($value); + } + } + + /** + * On increment item pre + * + * @param Event $event + * @return mixed + */ + public function onIncrementItemPre(Event $event) + { + $storage = $event->getTarget(); + $params = $event->getParams(); + $casToken = null; + $success = null; + $oldValue = $storage->getItem($params['key'], $success, $casToken); + $newValue = $oldValue + $params['value']; + + if ($success) { + $storage->checkAndSetItem($casToken, $params['key'], $oldValue + $params['value']); + $result = $newValue; + } else { + $result = false; + } + + $event->stopPropagation(true); + return $result; + } + + /** + * On increment items pre + * + * @param Event $event + * @return mixed + */ + public function onIncrementItemsPre(Event $event) + { + $storage = $event->getTarget(); + $params = $event->getParams(); + $keyValuePairs = $storage->getItems(array_keys($params['keyValuePairs'])); + foreach ($params['keyValuePairs'] as $key => & $value) { + if (isset($keyValuePairs[$key])) { + $keyValuePairs[$key]+= $value; + } else { + $keyValuePairs[$key] = $value; + } + } + + $failedKeys = $storage->setItems($keyValuePairs); + foreach ($failedKeys as $failedKey) { + unset($keyValuePairs[$failedKey]); + } + + $event->stopPropagation(true); + return $keyValuePairs; + } + + /** + * On decrement item pre + * + * @param Event $event + * @return mixed + */ + public function onDecrementItemPre(Event $event) + { + $storage = $event->getTarget(); + $params = $event->getParams(); + $success = null; + $casToken = null; + $oldValue = $storage->getItem($params['key'], $success, $casToken); + $newValue = $oldValue - $params['value']; + + if ($success) { + $storage->checkAndSetItem($casToken, $params['key'], $oldValue + $params['value']); + $result = $newValue; + } else { + $result = false; + } + + $event->stopPropagation(true); + return $result; + } + + /** + * On decrement items pre + * + * @param Event $event + * @return mixed + */ + public function onDecrementItemsPre(Event $event) + { + $storage = $event->getTarget(); + $params = $event->getParams(); + $keyValuePairs = $storage->getItems(array_keys($params['keyValuePairs'])); + foreach ($params['keyValuePairs'] as $key => &$value) { + if (isset($keyValuePairs[$key])) { + $keyValuePairs[$key]-= $value; + } else { + $keyValuePairs[$key] = -$value; + } + } + + $failedKeys = $storage->setItems($keyValuePairs); + foreach ($failedKeys as $failedKey) { + unset($keyValuePairs[$failedKey]); + } + + $event->stopPropagation(true); + return $keyValuePairs; + } + + /** + * On get capabilities + * + * @param PostEvent $event + * @return void + */ + public function onGetCapabilitiesPost(PostEvent $event) + { + $baseCapabilities = $event->getResult(); + $index = spl_object_hash($baseCapabilities); + + if (!isset($this->capabilities[$index])) { + $this->capabilities[$index] = new Capabilities( + $baseCapabilities->getAdapter(), + new stdClass(), // marker + array('supportedDatatypes' => array( + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => true, + 'object' => 'object', + 'resource' => false, + )), + $baseCapabilities + ); + } + + $event->setResult($this->capabilities[$index]); + } +} diff --git a/library/Zend/Cache/Storage/PluginManager.php b/library/Zend/Cache/Storage/PluginManager.php new file mode 100755 index 0000000000..a799a37d34 --- /dev/null +++ b/library/Zend/Cache/Storage/PluginManager.php @@ -0,0 +1,66 @@ + 'Zend\Cache\Storage\Plugin\ClearExpiredByFactor', + 'exceptionhandler' => 'Zend\Cache\Storage\Plugin\ExceptionHandler', + 'ignoreuserabort' => 'Zend\Cache\Storage\Plugin\IgnoreUserAbort', + 'optimizebyfactor' => 'Zend\Cache\Storage\Plugin\OptimizeByFactor', + 'serializer' => 'Zend\Cache\Storage\Plugin\Serializer', + ); + + /** + * Do not share by default + * + * @var array + */ + protected $shareByDefault = false; + + /** + * Validate the plugin + * + * Checks that the plugin loaded is an instance of Plugin\PluginInterface. + * + * @param mixed $plugin + * @return void + * @throws Exception\RuntimeException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof Plugin\PluginInterface) { + // we're okay + return; + } + + throw new Exception\RuntimeException(sprintf( + 'Plugin of type %s is invalid; must implement %s\Plugin\PluginInterface', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/library/Zend/Cache/Storage/PostEvent.php b/library/Zend/Cache/Storage/PostEvent.php new file mode 100755 index 0000000000..9302b0814e --- /dev/null +++ b/library/Zend/Cache/Storage/PostEvent.php @@ -0,0 +1,60 @@ +setResult($result); + } + + /** + * Set the result/return value + * + * @param mixed $value + * @return PostEvent + */ + public function setResult(& $value) + { + $this->result = & $value; + return $this; + } + + /** + * Get the result/return value + * + * @return mixed + */ + public function & getResult() + { + return $this->result; + } +} diff --git a/library/Zend/Cache/Storage/StorageInterface.php b/library/Zend/Cache/Storage/StorageInterface.php new file mode 100755 index 0000000000..c89ca5bef2 --- /dev/null +++ b/library/Zend/Cache/Storage/StorageInterface.php @@ -0,0 +1,246 @@ + $v) { + $pluginPrio = 1; // default priority + + if (is_string($k)) { + if (!is_array($v)) { + throw new Exception\InvalidArgumentException( + "'plugins.{$k}' needs to be an array" + ); + } + $pluginName = $k; + $pluginOptions = $v; + } elseif (is_array($v)) { + if (!isset($v['name'])) { + throw new Exception\InvalidArgumentException("Invalid plugins[{$k}] or missing plugins[{$k}].name"); + } + $pluginName = (string) $v['name']; + + if (isset($v['options'])) { + $pluginOptions = $v['options']; + } else { + $pluginOptions = array(); + } + + if (isset($v['priority'])) { + $pluginPrio = $v['priority']; + } + } else { + $pluginName = $v; + $pluginOptions = array(); + } + + $plugin = static::pluginFactory($pluginName, $pluginOptions); + $adapter->addPlugin($plugin, $pluginPrio); + } + } + + return $adapter; + } + + /** + * Instantiate a storage adapter + * + * @param string|Storage\StorageInterface $adapterName + * @param array|Traversable|Storage\Adapter\AdapterOptions $options + * @return Storage\StorageInterface + * @throws Exception\RuntimeException + */ + public static function adapterFactory($adapterName, $options = array()) + { + if ($adapterName instanceof Storage\StorageInterface) { + // $adapterName is already an adapter object + $adapter = $adapterName; + } else { + $adapter = static::getAdapterPluginManager()->get($adapterName); + } + + if ($options) { + $adapter->setOptions($options); + } + + return $adapter; + } + + /** + * Get the adapter plugin manager + * + * @return Storage\AdapterPluginManager + */ + public static function getAdapterPluginManager() + { + if (static::$adapters === null) { + static::$adapters = new Storage\AdapterPluginManager(); + } + return static::$adapters; + } + + /** + * Change the adapter plugin manager + * + * @param Storage\AdapterPluginManager $adapters + * @return void + */ + public static function setAdapterPluginManager(Storage\AdapterPluginManager $adapters) + { + static::$adapters = $adapters; + } + + /** + * Resets the internal adapter plugin manager + * + * @return void + */ + public static function resetAdapterPluginManager() + { + static::$adapters = null; + } + + /** + * Instantiate a storage plugin + * + * @param string|Storage\Plugin\PluginInterface $pluginName + * @param array|Traversable|Storage\Plugin\PluginOptions $options + * @return Storage\Plugin\PluginInterface + * @throws Exception\RuntimeException + */ + public static function pluginFactory($pluginName, $options = array()) + { + if ($pluginName instanceof Storage\Plugin\PluginInterface) { + // $pluginName is already a plugin object + $plugin = $pluginName; + } else { + $plugin = static::getPluginManager()->get($pluginName); + } + + if (!$options instanceof Storage\Plugin\PluginOptions) { + $options = new Storage\Plugin\PluginOptions($options); + } + + if ($options) { + $plugin->setOptions($options); + } + + return $plugin; + } + + /** + * Get the plugin manager + * + * @return Storage\PluginManager + */ + public static function getPluginManager() + { + if (static::$plugins === null) { + static::$plugins = new Storage\PluginManager(); + } + return static::$plugins; + } + + /** + * Change the plugin manager + * + * @param Storage\PluginManager $plugins + * @return void + */ + public static function setPluginManager(Storage\PluginManager $plugins) + { + static::$plugins = $plugins; + } + + /** + * Resets the internal plugin manager + * + * @return void + */ + public static function resetPluginManager() + { + static::$plugins = null; + } +} diff --git a/library/Zend/Cache/composer.json b/library/Zend/Cache/composer.json new file mode 100755 index 0000000000..3d20dff9a3 --- /dev/null +++ b/library/Zend/Cache/composer.json @@ -0,0 +1,40 @@ +{ + "name": "zendframework/zend-cache", + "description": "provides a generic way to cache any data", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "cache" + ], + "homepage": "https://github.com/zendframework/zf2", + "autoload": { + "psr-0": { + "Zend\\Cache\\": "" + } + }, + "target-dir": "Zend/Cache", + "require": { + "php": ">=5.3.23", + "zendframework/zend-stdlib": "self.version", + "zendframework/zend-servicemanager": "self.version", + "zendframework/zend-serializer": "self.version", + "zendframework/zend-eventmanager": "self.version" + }, + "require-dev": { + "zendframework/zend-session": "self.version" + }, + "suggest": { + "zendframework/zend-serializer": "Zend\\Serializer component", + "zendframework/zend-session": "Zend\\Session component", + "ext-apc": "APC >= 3.1.6 to use the APC storage adapter", + "ext-dba": "DBA, to use the DBA storage adapter", + "ext-memcached": "Memcached >= 1.0.0 to use the Memcached storage adapter", + "ext-wincache": "WinCache, to use the WinCache storage adapter" + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev", + "dev-develop": "2.4-dev" + } + } +} diff --git a/library/Zend/Captcha/AbstractAdapter.php b/library/Zend/Captcha/AbstractAdapter.php new file mode 100755 index 0000000000..11784d69e7 --- /dev/null +++ b/library/Zend/Captcha/AbstractAdapter.php @@ -0,0 +1,135 @@ +name; + } + + /** + * Set name + * + * @param string $name + * @return AbstractAdapter + */ + public function setName($name) + { + $this->name = $name; + return $this; + } + + /** + * Set single option for the object + * + * @param string $key + * @param string $value + * @return AbstractAdapter + */ + public function setOption($key, $value) + { + if (in_array(strtolower($key), $this->skipOptions)) { + return $this; + } + + $method = 'set' . ucfirst($key); + if (method_exists($this, $method)) { + // Setter exists; use it + $this->$method($value); + $this->options[$key] = $value; + } elseif (property_exists($this, $key)) { + // Assume it's metadata + $this->$key = $value; + $this->options[$key] = $value; + } + return $this; + } + + /** + * Set object state from options array + * + * @param array|Traversable $options + * @throws Exception\InvalidArgumentException + * @return AbstractAdapter + */ + public function setOptions($options = array()) + { + if (!is_array($options) && !$options instanceof Traversable) { + throw new Exception\InvalidArgumentException(__METHOD__ . ' expects an array or Traversable'); + } + + foreach ($options as $key => $value) { + $this->setOption($key, $value); + } + return $this; + } + + /** + * Retrieve options representing object state + * + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * Get helper name used to render captcha + * + * By default, return empty string, indicating no helper needed. + * + * @return string + */ + public function getHelperName() + { + return ''; + } +} diff --git a/library/Zend/Captcha/AbstractWord.php b/library/Zend/Captcha/AbstractWord.php new file mode 100755 index 0000000000..5ad8036611 --- /dev/null +++ b/library/Zend/Captcha/AbstractWord.php @@ -0,0 +1,407 @@ + 'Empty captcha value', + self::MISSING_ID => 'Captcha ID field is missing', + self::BAD_CAPTCHA => 'Captcha value is wrong', + ); + + /** + * Length of the word to generate + * + * @var int + */ + protected $wordlen = 8; + + /** + * Retrieve session class to utilize + * + * @return string + */ + public function getSessionClass() + { + return $this->sessionClass; + } + + /** + * Set session class for persistence + * + * @param string $sessionClass + * @return AbstractWord + */ + public function setSessionClass($sessionClass) + { + $this->sessionClass = $sessionClass; + return $this; + } + + /** + * Retrieve word length to use when generating captcha + * + * @return int + */ + public function getWordlen() + { + return $this->wordlen; + } + + /** + * Set word length of captcha + * + * @param int $wordlen + * @return AbstractWord + */ + public function setWordlen($wordlen) + { + $this->wordlen = $wordlen; + return $this; + } + + /** + * Retrieve captcha ID + * + * @return string + */ + public function getId() + { + if (null === $this->id) { + $this->setId($this->generateRandomId()); + } + return $this->id; + } + + /** + * Set captcha identifier + * + * @param string $id + * @return AbstractWord + */ + protected function setId($id) + { + $this->id = $id; + return $this; + } + + /** + * Set timeout for session token + * + * @param int $ttl + * @return AbstractWord + */ + public function setTimeout($ttl) + { + $this->timeout = (int) $ttl; + return $this; + } + + /** + * Get session token timeout + * + * @return int + */ + public function getTimeout() + { + return $this->timeout; + } + + /** + * Sets if session should be preserved on generate() + * + * @param bool $keepSession Should session be kept on generate()? + * @return AbstractWord + */ + public function setKeepSession($keepSession) + { + $this->keepSession = $keepSession; + return $this; + } + + /** + * Numbers should be included in the pattern? + * + * @return bool + */ + public function getUseNumbers() + { + return $this->useNumbers; + } + + /** + * Set if numbers should be included in the pattern + * + * @param bool $useNumbers numbers should be included in the pattern? + * @return AbstractWord + */ + public function setUseNumbers($useNumbers) + { + $this->useNumbers = $useNumbers; + return $this; + } + + /** + * Get session object + * + * @throws Exception\InvalidArgumentException + * @return Container + */ + public function getSession() + { + if (!isset($this->session) || (null === $this->session)) { + $id = $this->getId(); + if (!class_exists($this->sessionClass)) { + throw new Exception\InvalidArgumentException("Session class $this->sessionClass not found"); + } + $this->session = new $this->sessionClass('Zend_Form_Captcha_' . $id); + $this->session->setExpirationHops(1, null); + $this->session->setExpirationSeconds($this->getTimeout()); + } + return $this->session; + } + + /** + * Set session namespace object + * + * @param Container $session + * @return AbstractWord + */ + public function setSession(Container $session) + { + $this->session = $session; + if ($session) { + $this->keepSession = true; + } + return $this; + } + + /** + * Get captcha word + * + * @return string + */ + public function getWord() + { + if (empty($this->word)) { + $session = $this->getSession(); + $this->word = $session->word; + } + return $this->word; + } + + /** + * Set captcha word + * + * @param string $word + * @return AbstractWord + */ + protected function setWord($word) + { + $session = $this->getSession(); + $session->word = $word; + $this->word = $word; + return $this; + } + + /** + * Generate new random word + * + * @return string + */ + protected function generateWord() + { + $word = ''; + $wordLen = $this->getWordLen(); + $vowels = $this->useNumbers ? static::$VN : static::$V; + $consonants = $this->useNumbers ? static::$CN : static::$C; + + for ($i=0; $i < $wordLen; $i = $i + 2) { + // generate word with mix of vowels and consonants + $consonant = $consonants[array_rand($consonants)]; + $vowel = $vowels[array_rand($vowels)]; + $word .= $consonant . $vowel; + } + + if (strlen($word) > $wordLen) { + $word = substr($word, 0, $wordLen); + } + + return $word; + } + + /** + * Generate new session ID and new word + * + * @return string session ID + */ + public function generate() + { + if (!$this->keepSession) { + $this->session = null; + } + $id = $this->generateRandomId(); + $this->setId($id); + $word = $this->generateWord(); + $this->setWord($word); + return $id; + } + + /** + * Generate a random identifier + * + * @return string + */ + protected function generateRandomId() + { + return md5(Rand::getBytes(32)); + } + + /** + * Validate the word + * + * @see Zend\Validator\ValidatorInterface::isValid() + * @param mixed $value + * @param mixed $context + * @return bool + */ + public function isValid($value, $context = null) + { + if (!is_array($value)) { + if (!is_array($context)) { + $this->error(self::MISSING_VALUE); + return false; + } + $value = $context; + } + + $name = $this->getName(); + + if (isset($value[$name])) { + $value = $value[$name]; + } + + if (!isset($value['input'])) { + $this->error(self::MISSING_VALUE); + return false; + } + $input = strtolower($value['input']); + $this->setValue($input); + + if (!isset($value['id'])) { + $this->error(self::MISSING_ID); + return false; + } + + $this->id = $value['id']; + if ($input !== $this->getWord()) { + $this->error(self::BAD_CAPTCHA); + return false; + } + + return true; + } + + /** + * Get helper name used to render captcha + * + * @return string + */ + public function getHelperName() + { + return 'captcha/word'; + } +} diff --git a/library/Zend/Captcha/AdapterInterface.php b/library/Zend/Captcha/AdapterInterface.php new file mode 100755 index 0000000000..227daa5c76 --- /dev/null +++ b/library/Zend/Captcha/AdapterInterface.php @@ -0,0 +1,49 @@ +label = $label; + } + + /** + * Retrieve the label for the CAPTCHA + * @return string + */ + public function getLabel() + { + return $this->label; + } + + /** + * Retrieve optional view helper name to use when rendering this captcha + * + * @return string + */ + public function getHelperName() + { + return 'captcha/dumb'; + } +} diff --git a/library/Zend/Captcha/Exception/DomainException.php b/library/Zend/Captcha/Exception/DomainException.php new file mode 100755 index 0000000000..ac900097b4 --- /dev/null +++ b/library/Zend/Captcha/Exception/DomainException.php @@ -0,0 +1,14 @@ + 'Zend\Captcha\Dumb', + 'figlet' => 'Zend\Captcha\Figlet', + 'image' => 'Zend\Captcha\Image', + 'recaptcha' => 'Zend\Captcha\ReCaptcha', + ); + + /** + * Create a captcha adapter instance + * + * @param array|Traversable $options + * @return AdapterInterface + * @throws Exception\InvalidArgumentException for a non-array, non-Traversable $options + * @throws Exception\DomainException if class is missing or invalid + */ + public static function factory($options) + { + if ($options instanceof Traversable) { + $options = ArrayUtils::iteratorToArray($options); + } + + if (!is_array($options)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects an array or Traversable argument; received "%s"', + __METHOD__, + (is_object($options) ? get_class($options) : gettype($options)) + )); + } + + if (!isset($options['class'])) { + throw new Exception\DomainException(sprintf( + '%s expects a "class" attribute in the options; none provided', + __METHOD__ + )); + } + + $class = $options['class']; + if (isset(static::$classMap[strtolower($class)])) { + $class = static::$classMap[strtolower($class)]; + } + if (!class_exists($class)) { + throw new Exception\DomainException(sprintf( + '%s expects the "class" attribute to resolve to an existing class; received "%s"', + __METHOD__, + $class + )); + } + + unset($options['class']); + + if (isset($options['options'])) { + $options = $options['options']; + } + $captcha = new $class($options); + + if (!$captcha instanceof AdapterInterface) { + throw new Exception\DomainException(sprintf( + '%s expects the "class" attribute to resolve to a valid Zend\Captcha\AdapterInterface instance; received "%s"', + __METHOD__, + $class + )); + } + + return $captcha; + } +} diff --git a/library/Zend/Captcha/Figlet.php b/library/Zend/Captcha/Figlet.php new file mode 100755 index 0000000000..002f5f3072 --- /dev/null +++ b/library/Zend/Captcha/Figlet.php @@ -0,0 +1,69 @@ +figlet = new FigletManager($options); + } + + /** + * Retrieve the composed figlet manager + * + * @return FigletManager + */ + public function getFiglet() + { + return $this->figlet; + } + + /** + * Generate new captcha + * + * @return string + */ + public function generate() + { + $this->useNumbers = false; + return parent::generate(); + } + + /** + * Get helper name used to render captcha + * + * @return string + */ + public function getHelperName() + { + return 'captcha/figlet'; + } +} diff --git a/library/Zend/Captcha/Image.php b/library/Zend/Captcha/Image.php new file mode 100755 index 0000000000..7f91b9128d --- /dev/null +++ b/library/Zend/Captcha/Image.php @@ -0,0 +1,628 @@ +imgAlt; + } + + /** + * @return string + */ + public function getStartImage() + { + return $this->startImage; + } + + /** + * @return int + */ + public function getDotNoiseLevel() + { + return $this->dotNoiseLevel; + } + + /** + * @return int + */ + public function getLineNoiseLevel() + { + return $this->lineNoiseLevel; + } + + /** + * Get captcha expiration + * + * @return int + */ + public function getExpiration() + { + return $this->expiration; + } + + /** + * Get garbage collection frequency + * + * @return int + */ + public function getGcFreq() + { + return $this->gcFreq; + } + + /** + * Get font to use when generating captcha + * + * @return string + */ + public function getFont() + { + return $this->font; + } + + /** + * Get font size + * + * @return int + */ + public function getFontSize() + { + return $this->fsize; + } + + /** + * Get captcha image height + * + * @return int + */ + public function getHeight() + { + return $this->height; + } + + /** + * Get captcha image directory + * + * @return string + */ + public function getImgDir() + { + return $this->imgDir; + } + + /** + * Get captcha image base URL + * + * @return string + */ + public function getImgUrl() + { + return $this->imgUrl; + } + + /** + * Get captcha image file suffix + * + * @return string + */ + public function getSuffix() + { + return $this->suffix; + } + + /** + * Get captcha image width + * + * @return int + */ + public function getWidth() + { + return $this->width; + } + + /** + * @param string $startImage + * @return Image + */ + public function setStartImage($startImage) + { + $this->startImage = $startImage; + return $this; + } + + /** + * @param int $dotNoiseLevel + * @return Image + */ + public function setDotNoiseLevel($dotNoiseLevel) + { + $this->dotNoiseLevel = $dotNoiseLevel; + return $this; + } + + /** + * @param int $lineNoiseLevel + * @return Image + */ + public function setLineNoiseLevel($lineNoiseLevel) + { + $this->lineNoiseLevel = $lineNoiseLevel; + return $this; + } + + /** + * Set captcha expiration + * + * @param int $expiration + * @return Image + */ + public function setExpiration($expiration) + { + $this->expiration = $expiration; + return $this; + } + + /** + * Set garbage collection frequency + * + * @param int $gcFreq + * @return Image + */ + public function setGcFreq($gcFreq) + { + $this->gcFreq = $gcFreq; + return $this; + } + + /** + * Set captcha font + * + * @param string $font + * @return Image + */ + public function setFont($font) + { + $this->font = $font; + return $this; + } + + /** + * Set captcha font size + * + * @param int $fsize + * @return Image + */ + public function setFontSize($fsize) + { + $this->fsize = $fsize; + return $this; + } + + /** + * Set captcha image height + * + * @param int $height + * @return Image + */ + public function setHeight($height) + { + $this->height = $height; + return $this; + } + + /** + * Set captcha image storage directory + * + * @param string $imgDir + * @return Image + */ + public function setImgDir($imgDir) + { + $this->imgDir = rtrim($imgDir, "/\\") . '/'; + return $this; + } + + /** + * Set captcha image base URL + * + * @param string $imgUrl + * @return Image + */ + public function setImgUrl($imgUrl) + { + $this->imgUrl = rtrim($imgUrl, "/\\") . '/'; + return $this; + } + + /** + * @param string $imgAlt + * @return Image + */ + public function setImgAlt($imgAlt) + { + $this->imgAlt = $imgAlt; + return $this; + } + + /** + * Set captcha image filename suffix + * + * @param string $suffix + * @return Image + */ + public function setSuffix($suffix) + { + $this->suffix = $suffix; + return $this; + } + + /** + * Set captcha image width + * + * @param int $width + * @return Image + */ + public function setWidth($width) + { + $this->width = $width; + return $this; + } + + /** + * Generate random frequency + * + * @return float + */ + protected function randomFreq() + { + return mt_rand(700000, 1000000) / 15000000; + } + + /** + * Generate random phase + * + * @return float + */ + protected function randomPhase() + { + // random phase from 0 to pi + return mt_rand(0, 3141592) / 1000000; + } + + /** + * Generate random character size + * + * @return int + */ + protected function randomSize() + { + return mt_rand(300, 700) / 100; + } + + /** + * Generate captcha + * + * @return string captcha ID + */ + public function generate() + { + $id = parent::generate(); + $tries = 5; + + // If there's already such file, try creating a new ID + while ($tries-- && file_exists($this->getImgDir() . $id . $this->getSuffix())) { + $id = $this->generateRandomId(); + $this->setId($id); + } + $this->generateImage($id, $this->getWord()); + + if (mt_rand(1, $this->getGcFreq()) == 1) { + $this->gc(); + } + + return $id; + } + + /** + * Generate image captcha + * + * Override this function if you want different image generator + * Wave transform from http://www.captcha.ru/captchas/multiwave/ + * + * @param string $id Captcha ID + * @param string $word Captcha word + * @throws Exception\NoFontProvidedException if no font was set + * @throws Exception\ImageNotLoadableException if start image cannot be loaded + */ + protected function generateImage($id, $word) + { + $font = $this->getFont(); + + if (empty($font)) { + throw new Exception\NoFontProvidedException('Image CAPTCHA requires font'); + } + + $w = $this->getWidth(); + $h = $this->getHeight(); + $fsize = $this->getFontSize(); + + $imgFile = $this->getImgDir() . $id . $this->getSuffix(); + + if (empty($this->startImage)) { + $img = imagecreatetruecolor($w, $h); + } else { + // Potential error is change to exception + ErrorHandler::start(); + $img = imagecreatefrompng($this->startImage); + $error = ErrorHandler::stop(); + if (!$img || $error) { + throw new Exception\ImageNotLoadableException( + "Can not load start image '{$this->startImage}'", + 0, + $error + ); + } + $w = imagesx($img); + $h = imagesy($img); + } + + $textColor = imagecolorallocate($img, 0, 0, 0); + $bgColor = imagecolorallocate($img, 255, 255, 255); + imagefilledrectangle($img, 0, 0, $w-1, $h-1, $bgColor); + $textbox = imageftbbox($fsize, 0, $font, $word); + $x = ($w - ($textbox[2] - $textbox[0])) / 2; + $y = ($h - ($textbox[7] - $textbox[1])) / 2; + imagefttext($img, $fsize, 0, $x, $y, $textColor, $font, $word); + + // generate noise + for ($i=0; $i < $this->dotNoiseLevel; $i++) { + imagefilledellipse($img, mt_rand(0, $w), mt_rand(0, $h), 2, 2, $textColor); + } + for ($i=0; $i < $this->lineNoiseLevel; $i++) { + imageline($img, mt_rand(0, $w), mt_rand(0, $h), mt_rand(0, $w), mt_rand(0, $h), $textColor); + } + + // transformed image + $img2 = imagecreatetruecolor($w, $h); + $bgColor = imagecolorallocate($img2, 255, 255, 255); + imagefilledrectangle($img2, 0, 0, $w-1, $h-1, $bgColor); + + // apply wave transforms + $freq1 = $this->randomFreq(); + $freq2 = $this->randomFreq(); + $freq3 = $this->randomFreq(); + $freq4 = $this->randomFreq(); + + $ph1 = $this->randomPhase(); + $ph2 = $this->randomPhase(); + $ph3 = $this->randomPhase(); + $ph4 = $this->randomPhase(); + + $szx = $this->randomSize(); + $szy = $this->randomSize(); + + for ($x = 0; $x < $w; $x++) { + for ($y = 0; $y < $h; $y++) { + $sx = $x + (sin($x*$freq1 + $ph1) + sin($y*$freq3 + $ph3)) * $szx; + $sy = $y + (sin($x*$freq2 + $ph2) + sin($y*$freq4 + $ph4)) * $szy; + + if ($sx < 0 || $sy < 0 || $sx >= $w - 1 || $sy >= $h - 1) { + continue; + } else { + $color = (imagecolorat($img, $sx, $sy) >> 16) & 0xFF; + $colorX = (imagecolorat($img, $sx + 1, $sy) >> 16) & 0xFF; + $colorY = (imagecolorat($img, $sx, $sy + 1) >> 16) & 0xFF; + $colorXY = (imagecolorat($img, $sx + 1, $sy + 1) >> 16) & 0xFF; + } + + if ($color == 255 && $colorX == 255 && $colorY == 255 && $colorXY == 255) { + // ignore background + continue; + } elseif ($color == 0 && $colorX == 0 && $colorY == 0 && $colorXY == 0) { + // transfer inside of the image as-is + $newcolor = 0; + } else { + // do antialiasing for border items + $fracX = $sx - floor($sx); + $fracY = $sy - floor($sy); + $fracX1 = 1 - $fracX; + $fracY1 = 1 - $fracY; + + $newcolor = $color * $fracX1 * $fracY1 + + $colorX * $fracX * $fracY1 + + $colorY * $fracX1 * $fracY + + $colorXY * $fracX * $fracY; + } + + imagesetpixel($img2, $x, $y, imagecolorallocate($img2, $newcolor, $newcolor, $newcolor)); + } + } + + // generate noise + for ($i=0; $i<$this->dotNoiseLevel; $i++) { + imagefilledellipse($img2, mt_rand(0, $w), mt_rand(0, $h), 2, 2, $textColor); + } + + for ($i=0; $i<$this->lineNoiseLevel; $i++) { + imageline($img2, mt_rand(0, $w), mt_rand(0, $h), mt_rand(0, $w), mt_rand(0, $h), $textColor); + } + + imagepng($img2, $imgFile); + imagedestroy($img); + imagedestroy($img2); + } + + /** + * Remove old files from image directory + * + */ + protected function gc() + { + $expire = time() - $this->getExpiration(); + $imgdir = $this->getImgDir(); + if (!$imgdir || strlen($imgdir) < 2) { + // safety guard + return; + } + + $suffixLength = strlen($this->suffix); + foreach (new DirectoryIterator($imgdir) as $file) { + if (!$file->isDot() && !$file->isDir()) { + if (file_exists($file->getPathname()) && $file->getMTime() < $expire) { + // only deletes files ending with $this->suffix + if (substr($file->getFilename(), -($suffixLength)) == $this->suffix) { + unlink($file->getPathname()); + } + } + } + } + } + + /** + * Get helper name used to render captcha + * + * @return string + */ + public function getHelperName() + { + return 'captcha/image'; + } +} diff --git a/library/Zend/Captcha/README.md b/library/Zend/Captcha/README.md new file mode 100755 index 0000000000..1240158c59 --- /dev/null +++ b/library/Zend/Captcha/README.md @@ -0,0 +1,15 @@ +Captcha Component from ZF2 +========================== + +This is the Captcha component for ZF2. + +- File issues at https://github.com/zendframework/zf2/issues +- Create pull requests against https://github.com/zendframework/zf2 +- Documentation is at http://framework.zend.com/docs + +LICENSE +------- + +The files in this archive are released under the [Zend Framework +license](http://framework.zend.com/license), which is a 3-clause BSD license. + diff --git a/library/Zend/Captcha/ReCaptcha.php b/library/Zend/Captcha/ReCaptcha.php new file mode 100755 index 0000000000..7e5c019348 --- /dev/null +++ b/library/Zend/Captcha/ReCaptcha.php @@ -0,0 +1,247 @@ + 'Missing captcha fields', + self::ERR_CAPTCHA => 'Failed to validate captcha', + self::BAD_CAPTCHA => 'Captcha value is wrong: %value%', + ); + + /** + * Retrieve ReCaptcha Private key + * + * @return string + */ + public function getPrivkey() + { + return $this->getService()->getPrivateKey(); + } + + /** + * Retrieve ReCaptcha Public key + * + * @return string + */ + public function getPubkey() + { + return $this->getService()->getPublicKey(); + } + + /** + * Set ReCaptcha Private key + * + * @param string $privkey + * @return ReCaptcha + */ + public function setPrivkey($privkey) + { + $this->getService()->setPrivateKey($privkey); + return $this; + } + + /** + * Set ReCaptcha public key + * + * @param string $pubkey + * @return ReCaptcha + */ + public function setPubkey($pubkey) + { + $this->getService()->setPublicKey($pubkey); + return $this; + } + + /** + * Constructor + * + * @param null|array|Traversable $options + */ + public function __construct($options = null) + { + $this->setService(new ReCaptchaService()); + $this->serviceParams = $this->getService()->getParams(); + $this->serviceOptions = $this->getService()->getOptions(); + + parent::__construct($options); + + if (!empty($options)) { + if (array_key_exists('private_key', $options)) { + $this->getService()->setPrivateKey($options['private_key']); + } + if (array_key_exists('public_key', $options)) { + $this->getService()->setPublicKey($options['public_key']); + } + $this->setOptions($options); + } + } + + /** + * Set service object + * + * @param ReCaptchaService $service + * @return ReCaptcha + */ + public function setService(ReCaptchaService $service) + { + $this->service = $service; + return $this; + } + + /** + * Retrieve ReCaptcha service object + * + * @return ReCaptchaService + */ + public function getService() + { + return $this->service; + } + + /** + * Set option + * + * If option is a service parameter, proxies to the service. The same + * goes for any service options (distinct from service params) + * + * @param string $key + * @param mixed $value + * @return ReCaptcha + */ + public function setOption($key, $value) + { + $service = $this->getService(); + if (isset($this->serviceParams[$key])) { + $service->setParam($key, $value); + return $this; + } + if (isset($this->serviceOptions[$key])) { + $service->setOption($key, $value); + return $this; + } + return parent::setOption($key, $value); + } + + /** + * Generate captcha + * + * @see AbstractAdapter::generate() + * @return string + */ + public function generate() + { + return ""; + } + + /** + * Validate captcha + * + * @see \Zend\Validator\ValidatorInterface::isValid() + * @param mixed $value + * @param mixed $context + * @return bool + */ + public function isValid($value, $context = null) + { + if (!is_array($value) && !is_array($context)) { + $this->error(self::MISSING_VALUE); + return false; + } + + if (!is_array($value) && is_array($context)) { + $value = $context; + } + + if (empty($value[$this->CHALLENGE]) || empty($value[$this->RESPONSE])) { + $this->error(self::MISSING_VALUE); + return false; + } + + $service = $this->getService(); + + $res = $service->verify($value[$this->CHALLENGE], $value[$this->RESPONSE]); + + if (!$res) { + $this->error(self::ERR_CAPTCHA); + return false; + } + + if (!$res->isValid()) { + $this->error(self::BAD_CAPTCHA, $res->getErrorCode()); + $service->setParam('error', $res->getErrorCode()); + return false; + } + + return true; + } + + /** + * Get helper name used to render captcha + * + * @return string + */ + public function getHelperName() + { + return "captcha/recaptcha"; + } +} diff --git a/library/Zend/Captcha/composer.json b/library/Zend/Captcha/composer.json new file mode 100755 index 0000000000..fec1684fe5 --- /dev/null +++ b/library/Zend/Captcha/composer.json @@ -0,0 +1,40 @@ +{ + "name": "zendframework/zend-captcha", + "description": " ", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "captcha" + ], + "homepage": "https://github.com/zendframework/zf2", + "autoload": { + "psr-0": { + "Zend\\Captcha\\": "" + } + }, + "target-dir": "Zend/Captcha", + "require": { + "php": ">=5.3.23", + "zendframework/zend-math": "self.version", + "zendframework/zend-stdlib": "self.version" + }, + "require-dev": { + "zendframework/zend-session": "self.version", + "zendframework/zend-text": "self.version", + "zendframework/zend-validator": "self.version", + "zendframework/zendservice-recaptcha": "*" + }, + "suggest": { + "zendframework/zend-resources": "Translations of captcha messages", + "zendframework/zend-session": "Zend\\Session component", + "zendframework/zend-text": "Zend\\Text component", + "zendframework/zend-validator": "Zend\\Validator component", + "zendframework/zendservice-recaptcha": "ZendService\\ReCaptcha component" + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev", + "dev-develop": "2.4-dev" + } + } +} diff --git a/library/Zend/Code/Annotation/AnnotationCollection.php b/library/Zend/Code/Annotation/AnnotationCollection.php new file mode 100755 index 0000000000..7b9aa51da6 --- /dev/null +++ b/library/Zend/Code/Annotation/AnnotationCollection.php @@ -0,0 +1,32 @@ +setIdentifiers(array( + __CLASS__, + get_class($this), + )); + $this->events = $events; + + return $this; + } + + /** + * Retrieve event manager + * + * Lazy loads an instance if none registered. + * + * @return EventManagerInterface + */ + public function getEventManager() + { + if (null === $this->events) { + $this->setEventManager(new EventManager()); + } + + return $this->events; + } + + /** + * Attach a parser to listen to the createAnnotation event + * + * @param ParserInterface $parser + * @return AnnotationManager + */ + public function attach(ParserInterface $parser) + { + $this->getEventManager() + ->attach(self::EVENT_CREATE_ANNOTATION, array($parser, 'onCreateAnnotation')); + + return $this; + } + + /** + * Create Annotation + * + * @param string[] $annotationData + * @return false|\stdClass + */ + public function createAnnotation(array $annotationData) + { + $event = new Event(); + $event->setName(self::EVENT_CREATE_ANNOTATION); + $event->setTarget($this); + $event->setParams(array( + 'class' => $annotationData[0], + 'content' => $annotationData[1], + 'raw' => $annotationData[2], + )); + + $eventManager = $this->getEventManager(); + $results = $eventManager->trigger($event, function ($r) { + return (is_object($r)); + }); + + $annotation = $results->last(); + + return (is_object($annotation) ? $annotation : false); + } +} diff --git a/library/Zend/Code/Annotation/Parser/DoctrineAnnotationParser.php b/library/Zend/Code/Annotation/Parser/DoctrineAnnotationParser.php new file mode 100755 index 0000000000..5168e3278c --- /dev/null +++ b/library/Zend/Code/Annotation/Parser/DoctrineAnnotationParser.php @@ -0,0 +1,153 @@ +docParser = $docParser; + return $this; + } + + /** + * Retrieve the DocParser instance + * + * If none is registered, lazy-loads a new instance. + * + * @return DocParser + */ + public function getDocParser() + { + if (!$this->docParser instanceof DocParser) { + $this->setDocParser(new DocParser()); + } + + return $this->docParser; + } + + /** + * Handle annotation creation + * + * @param EventInterface $e + * @return false|\stdClass + */ + public function onCreateAnnotation(EventInterface $e) + { + $annotationClass = $e->getParam('class', false); + if (!$annotationClass) { + return false; + } + + if (!isset($this->allowedAnnotations[$annotationClass])) { + return false; + } + + $annotationString = $e->getParam('raw', false); + if (!$annotationString) { + return false; + } + + // Annotation classes provided by the AnnotationScanner are already + // resolved to fully-qualified class names. Adding the global namespace + // prefix allows the Doctrine annotation parser to locate the annotation + // class correctly. + $annotationString = preg_replace('/^(@)/', '$1\\', $annotationString); + + $parser = $this->getDocParser(); + $annotations = $parser->parse($annotationString); + if (empty($annotations)) { + return false; + } + + $annotation = array_shift($annotations); + if (!is_object($annotation)) { + return false; + } + + return $annotation; + } + + /** + * Specify an allowed annotation class + * + * @param string $annotation + * @return DoctrineAnnotationParser + */ + public function registerAnnotation($annotation) + { + $this->allowedAnnotations[$annotation] = true; + return $this; + } + + /** + * Set many allowed annotations at once + * + * @param array|Traversable $annotations Array or traversable object of + * annotation class names + * @throws Exception\InvalidArgumentException + * @return DoctrineAnnotationParser + */ + public function registerAnnotations($annotations) + { + if (!is_array($annotations) && !$annotations instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects an array or Traversable; received "%s"', + __METHOD__, + (is_object($annotations) ? get_class($annotations) : gettype($annotations)) + )); + } + + foreach ($annotations as $annotation) { + $this->allowedAnnotations[$annotation] = true; + } + + return $this; + } +} diff --git a/library/Zend/Code/Annotation/Parser/GenericAnnotationParser.php b/library/Zend/Code/Annotation/Parser/GenericAnnotationParser.php new file mode 100755 index 0000000000..b3f7566247 --- /dev/null +++ b/library/Zend/Code/Annotation/Parser/GenericAnnotationParser.php @@ -0,0 +1,224 @@ +getParam('class', false); + if (!$class || !$this->hasAnnotation($class)) { + return false; + } + + $content = $e->getParam('content', ''); + $content = trim($content, '()'); + + if ($this->hasAlias($class)) { + $class = $this->resolveAlias($class); + } + + $index = array_search($class, $this->annotationNames); + $annotation = $this->annotations[$index]; + + $newAnnotation = clone $annotation; + if ($content) { + $newAnnotation->initialize($content); + } + + return $newAnnotation; + } + + /** + * Register annotations + * + * @param string|AnnotationInterface $annotation String class name of an + * AnnotationInterface implementation, or actual instance + * @return GenericAnnotationParser + * @throws Exception\InvalidArgumentException + */ + public function registerAnnotation($annotation) + { + $class = false; + if (is_string($annotation) && class_exists($annotation)) { + $class = $annotation; + $annotation = new $annotation(); + } + + if (!$annotation instanceof AnnotationInterface) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects an instance of %s\AnnotationInterface; received "%s"', + __METHOD__, + __NAMESPACE__, + (is_object($annotation) ? get_class($annotation) : gettype($annotation)) + )); + } + + $class = $class ?: get_class($annotation); + + if (in_array($class, $this->annotationNames)) { + throw new Exception\InvalidArgumentException(sprintf( + 'An annotation for this class %s already exists', + $class + )); + } + + $this->annotations[] = $annotation; + $this->annotationNames[] = $class; + } + + /** + * Register many annotations at once + * + * @param array|Traversable $annotations + * @throws Exception\InvalidArgumentException + * @return GenericAnnotationParser + */ + public function registerAnnotations($annotations) + { + if (!is_array($annotations) && !$annotations instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects an array or Traversable; received "%s"', + __METHOD__, + (is_object($annotations) ? get_class($annotations) : gettype($annotations)) + )); + } + + foreach ($annotations as $annotation) { + $this->registerAnnotation($annotation); + } + + return $this; + } + + /** + * Checks if the manager has annotations for a class + * + * @param string $class + * @return bool + */ + public function hasAnnotation($class) + { + if (in_array($class, $this->annotationNames)) { + return true; + } + + if ($this->hasAlias($class)) { + return true; + } + + return false; + } + + /** + * Alias an annotation name + * + * @param string $alias + * @param string $class May be either a registered annotation name or another alias + * @throws Exception\InvalidArgumentException + * @return GenericAnnotationParser + */ + public function setAlias($alias, $class) + { + if (!in_array($class, $this->annotationNames) && !$this->hasAlias($class)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: Cannot alias "%s" to "%s", as class "%s" is not currently a registered annotation or alias', + __METHOD__, + $alias, + $class, + $class + )); + } + + $alias = $this->normalizeAlias($alias); + $this->aliases[$alias] = $class; + + return $this; + } + + /** + * Normalize an alias name + * + * @param string $alias + * @return string + */ + protected function normalizeAlias($alias) + { + return strtolower(str_replace(array('-', '_', ' ', '\\', '/'), '', $alias)); + } + + /** + * Do we have an alias by the provided name? + * + * @param string $alias + * @return bool + */ + protected function hasAlias($alias) + { + $alias = $this->normalizeAlias($alias); + + return (isset($this->aliases[$alias])); + } + + /** + * Resolve an alias to a class name + * + * @param string $alias + * @return string + */ + protected function resolveAlias($alias) + { + do { + $normalized = $this->normalizeAlias($alias); + $class = $this->aliases[$normalized]; + } while ($this->hasAlias($class)); + + return $class; + } +} diff --git a/library/Zend/Code/Annotation/Parser/ParserInterface.php b/library/Zend/Code/Annotation/Parser/ParserInterface.php new file mode 100755 index 0000000000..bb6746e80c --- /dev/null +++ b/library/Zend/Code/Annotation/Parser/ParserInterface.php @@ -0,0 +1,39 @@ +setOptions($options); + } + } + + /** + * @param bool $isSourceDirty + * @return AbstractGenerator + */ + public function setSourceDirty($isSourceDirty = true) + { + $this->isSourceDirty = (bool) $isSourceDirty; + return $this; + } + + /** + * @return bool + */ + public function isSourceDirty() + { + return $this->isSourceDirty; + } + + /** + * @param string $indentation + * @return AbstractGenerator + */ + public function setIndentation($indentation) + { + $this->indentation = (string) $indentation; + return $this; + } + + /** + * @return string + */ + public function getIndentation() + { + return $this->indentation; + } + + /** + * @param string $sourceContent + * @return AbstractGenerator + */ + public function setSourceContent($sourceContent) + { + $this->sourceContent = (string) $sourceContent; + return $this; + } + + /** + * @return string + */ + public function getSourceContent() + { + return $this->sourceContent; + } + + /** + * @param array|Traversable $options + * @throws Exception\InvalidArgumentException + * @return AbstractGenerator + */ + public function setOptions($options) + { + if (!is_array($options) && !$options instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects an array or Traversable object; received "%s"', + __METHOD__, + (is_object($options) ? get_class($options) : gettype($options)) + )); + } + + foreach ($options as $optionName => $optionValue) { + $methodName = 'set' . $optionName; + if (method_exists($this, $methodName)) { + $this->{$methodName}($optionValue); + } + } + + return $this; + } +} diff --git a/library/Zend/Code/Generator/AbstractMemberGenerator.php b/library/Zend/Code/Generator/AbstractMemberGenerator.php new file mode 100755 index 0000000000..dec0d8dc06 --- /dev/null +++ b/library/Zend/Code/Generator/AbstractMemberGenerator.php @@ -0,0 +1,224 @@ +flags = $flags; + + return $this; + } + + /** + * @param int $flag + * @return AbstractMemberGenerator + */ + public function addFlag($flag) + { + $this->setFlags($this->flags | $flag); + return $this; + } + + /** + * @param int $flag + * @return AbstractMemberGenerator + */ + public function removeFlag($flag) + { + $this->setFlags($this->flags & ~$flag); + return $this; + } + + /** + * @param bool $isAbstract + * @return AbstractMemberGenerator + */ + public function setAbstract($isAbstract) + { + return (($isAbstract) ? $this->addFlag(self::FLAG_ABSTRACT) : $this->removeFlag(self::FLAG_ABSTRACT)); + } + + /** + * @return bool + */ + public function isAbstract() + { + return (bool) ($this->flags & self::FLAG_ABSTRACT); + } + + /** + * @param bool $isFinal + * @return AbstractMemberGenerator + */ + public function setFinal($isFinal) + { + return (($isFinal) ? $this->addFlag(self::FLAG_FINAL) : $this->removeFlag(self::FLAG_FINAL)); + } + + /** + * @return bool + */ + public function isFinal() + { + return (bool) ($this->flags & self::FLAG_FINAL); + } + + /** + * @param bool $isStatic + * @return AbstractMemberGenerator + */ + public function setStatic($isStatic) + { + return (($isStatic) ? $this->addFlag(self::FLAG_STATIC) : $this->removeFlag(self::FLAG_STATIC)); + } + + /** + * @return bool + */ + public function isStatic() + { + return (bool) ($this->flags & self::FLAG_STATIC); // is FLAG_STATIC in flags + } + + /** + * @param string $visibility + * @return AbstractMemberGenerator + */ + public function setVisibility($visibility) + { + switch ($visibility) { + case self::VISIBILITY_PUBLIC: + $this->removeFlag(self::FLAG_PRIVATE | self::FLAG_PROTECTED); // remove both + $this->addFlag(self::FLAG_PUBLIC); + break; + case self::VISIBILITY_PROTECTED: + $this->removeFlag(self::FLAG_PUBLIC | self::FLAG_PRIVATE); // remove both + $this->addFlag(self::FLAG_PROTECTED); + break; + case self::VISIBILITY_PRIVATE: + $this->removeFlag(self::FLAG_PUBLIC | self::FLAG_PROTECTED); // remove both + $this->addFlag(self::FLAG_PRIVATE); + break; + } + + return $this; + } + + /** + * @return string + */ + public function getVisibility() + { + switch (true) { + case ($this->flags & self::FLAG_PROTECTED): + return self::VISIBILITY_PROTECTED; + case ($this->flags & self::FLAG_PRIVATE): + return self::VISIBILITY_PRIVATE; + default: + return self::VISIBILITY_PUBLIC; + } + } + + /** + * @param string $name + * @return AbstractMemberGenerator + */ + public function setName($name) + { + $this->name = (string) $name; + return $this; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param DocBlockGenerator|string $docBlock + * @throws Exception\InvalidArgumentException + * @return AbstractMemberGenerator + */ + public function setDocBlock($docBlock) + { + if (is_string($docBlock)) { + $docBlock = new DocBlockGenerator($docBlock); + } elseif (!$docBlock instanceof DocBlockGenerator) { + throw new Exception\InvalidArgumentException(sprintf( + '%s is expecting either a string, array or an instance of %s\DocBlockGenerator', + __METHOD__, + __NAMESPACE__ + )); + } + + $this->docBlock = $docBlock; + + return $this; + } + + /** + * @return DocBlockGenerator + */ + public function getDocBlock() + { + return $this->docBlock; + } +} diff --git a/library/Zend/Code/Generator/BodyGenerator.php b/library/Zend/Code/Generator/BodyGenerator.php new file mode 100755 index 0000000000..4851e5ca4d --- /dev/null +++ b/library/Zend/Code/Generator/BodyGenerator.php @@ -0,0 +1,44 @@ +content = (string) $content; + return $this; + } + + /** + * @return string + */ + public function getContent() + { + return $this->content; + } + + /** + * @return string + */ + public function generate() + { + return $this->getContent(); + } +} diff --git a/library/Zend/Code/Generator/ClassGenerator.php b/library/Zend/Code/Generator/ClassGenerator.php new file mode 100755 index 0000000000..e845587466 --- /dev/null +++ b/library/Zend/Code/Generator/ClassGenerator.php @@ -0,0 +1,747 @@ +getName()); + + $cg->setSourceContent($cg->getSourceContent()); + $cg->setSourceDirty(false); + + if ($classReflection->getDocComment() != '') { + $cg->setDocBlock(DocBlockGenerator::fromReflection($classReflection->getDocBlock())); + } + + $cg->setAbstract($classReflection->isAbstract()); + + // set the namespace + if ($classReflection->inNamespace()) { + $cg->setNamespaceName($classReflection->getNamespaceName()); + } + + /* @var \Zend\Code\Reflection\ClassReflection $parentClass */ + $parentClass = $classReflection->getParentClass(); + if ($parentClass) { + $cg->setExtendedClass($parentClass->getName()); + $interfaces = array_diff($classReflection->getInterfaces(), $parentClass->getInterfaces()); + } else { + $interfaces = $classReflection->getInterfaces(); + } + + $interfaceNames = array(); + foreach ($interfaces as $interface) { + /* @var \Zend\Code\Reflection\ClassReflection $interface */ + $interfaceNames[] = $interface->getName(); + } + + $cg->setImplementedInterfaces($interfaceNames); + + $properties = array(); + foreach ($classReflection->getProperties() as $reflectionProperty) { + if ($reflectionProperty->getDeclaringClass()->getName() == $classReflection->getName()) { + $properties[] = PropertyGenerator::fromReflection($reflectionProperty); + } + } + $cg->addProperties($properties); + + $methods = array(); + foreach ($classReflection->getMethods() as $reflectionMethod) { + $className = ($cg->getNamespaceName())? $cg->getNamespaceName() . "\\" . $cg->getName() : $cg->getName(); + if ($reflectionMethod->getDeclaringClass()->getName() == $className) { + $methods[] = MethodGenerator::fromReflection($reflectionMethod); + } + } + $cg->addMethods($methods); + + return $cg; + } + + /** + * Generate from array + * + * @configkey name string [required] Class Name + * @configkey filegenerator FileGenerator File generator that holds this class + * @configkey namespacename string The namespace for this class + * @configkey docblock string The docblock information + * @configkey flags int Flags, one of ClassGenerator::FLAG_ABSTRACT ClassGenerator::FLAG_FINAL + * @configkey extendedclass string Class which this class is extending + * @configkey implementedinterfaces + * @configkey properties + * @configkey methods + * + * @throws Exception\InvalidArgumentException + * @param array $array + * @return ClassGenerator + */ + public static function fromArray(array $array) + { + if (!isset($array['name'])) { + throw new Exception\InvalidArgumentException( + 'Class generator requires that a name is provided for this object' + ); + } + + $cg = new static($array['name']); + foreach ($array as $name => $value) { + // normalize key + switch (strtolower(str_replace(array('.', '-', '_'), '', $name))) { + case 'containingfile': + $cg->setContainingFileGenerator($value); + break; + case 'namespacename': + $cg->setNamespaceName($value); + break; + case 'docblock': + $docBlock = ($value instanceof DocBlockGenerator) ? $value : DocBlockGenerator::fromArray($value); + $cg->setDocBlock($docBlock); + break; + case 'flags': + $cg->setFlags($value); + break; + case 'extendedclass': + $cg->setExtendedClass($value); + break; + case 'implementedinterfaces': + $cg->setImplementedInterfaces($value); + break; + case 'properties': + $cg->addProperties($value); + break; + case 'methods': + $cg->addMethods($value); + break; + } + } + + return $cg; + } + + /** + * @param string $name + * @param string $namespaceName + * @param array|string $flags + * @param string $extends + * @param array $interfaces + * @param array $properties + * @param array $methods + * @param DocBlockGenerator $docBlock + */ + public function __construct( + $name = null, + $namespaceName = null, + $flags = null, + $extends = null, + $interfaces = array(), + $properties = array(), + $methods = array(), + $docBlock = null + ) { + if ($name !== null) { + $this->setName($name); + } + if ($namespaceName !== null) { + $this->setNamespaceName($namespaceName); + } + if ($flags !== null) { + $this->setFlags($flags); + } + if ($properties !== array()) { + $this->addProperties($properties); + } + if ($extends !== null) { + $this->setExtendedClass($extends); + } + if (is_array($interfaces)) { + $this->setImplementedInterfaces($interfaces); + } + if ($methods !== array()) { + $this->addMethods($methods); + } + if ($docBlock !== null) { + $this->setDocBlock($docBlock); + } + } + + /** + * @param string $name + * @return ClassGenerator + */ + public function setName($name) + { + if (strstr($name, '\\')) { + $namespace = substr($name, 0, strrpos($name, '\\')); + $name = substr($name, strrpos($name, '\\') + 1); + $this->setNamespaceName($namespace); + } + + $this->name = $name; + return $this; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $namespaceName + * @return ClassGenerator + */ + public function setNamespaceName($namespaceName) + { + $this->namespaceName = $namespaceName; + return $this; + } + + /** + * @return string + */ + public function getNamespaceName() + { + return $this->namespaceName; + } + + /** + * @param FileGenerator $fileGenerator + * @return ClassGenerator + */ + public function setContainingFileGenerator(FileGenerator $fileGenerator) + { + $this->containingFileGenerator = $fileGenerator; + return $this; + } + + /** + * @return FileGenerator + */ + public function getContainingFileGenerator() + { + return $this->containingFileGenerator; + } + + /** + * @param DocBlockGenerator $docBlock + * @return ClassGenerator + */ + public function setDocBlock(DocBlockGenerator $docBlock) + { + $this->docBlock = $docBlock; + return $this; + } + + /** + * @return DocBlockGenerator + */ + public function getDocBlock() + { + return $this->docBlock; + } + + /** + * @param array|string $flags + * @return ClassGenerator + */ + public function setFlags($flags) + { + if (is_array($flags)) { + $flagsArray = $flags; + $flags = 0x00; + foreach ($flagsArray as $flag) { + $flags |= $flag; + } + } + // check that visibility is one of three + $this->flags = $flags; + + return $this; + } + + /** + * @param string $flag + * @return ClassGenerator + */ + public function addFlag($flag) + { + $this->setFlags($this->flags | $flag); + return $this; + } + + /** + * @param string $flag + * @return ClassGenerator + */ + public function removeFlag($flag) + { + $this->setFlags($this->flags & ~$flag); + return $this; + } + + /** + * @param bool $isAbstract + * @return ClassGenerator + */ + public function setAbstract($isAbstract) + { + return (($isAbstract) ? $this->addFlag(self::FLAG_ABSTRACT) : $this->removeFlag(self::FLAG_ABSTRACT)); + } + + /** + * @return bool + */ + public function isAbstract() + { + return (bool) ($this->flags & self::FLAG_ABSTRACT); + } + + /** + * @param bool $isFinal + * @return ClassGenerator + */ + public function setFinal($isFinal) + { + return (($isFinal) ? $this->addFlag(self::FLAG_FINAL) : $this->removeFlag(self::FLAG_FINAL)); + } + + /** + * @return bool + */ + public function isFinal() + { + return ($this->flags & self::FLAG_FINAL); + } + + /** + * @param string $extendedClass + * @return ClassGenerator + */ + public function setExtendedClass($extendedClass) + { + $this->extendedClass = $extendedClass; + return $this; + } + + /** + * @return string + */ + public function getExtendedClass() + { + return $this->extendedClass; + } + + /** + * @param array $implementedInterfaces + * @return ClassGenerator + */ + public function setImplementedInterfaces(array $implementedInterfaces) + { + $this->implementedInterfaces = $implementedInterfaces; + return $this; + } + + /** + * @return array + */ + public function getImplementedInterfaces() + { + return $this->implementedInterfaces; + } + + /** + * @param array $properties + * @return ClassGenerator + */ + public function addProperties(array $properties) + { + foreach ($properties as $property) { + if ($property instanceof PropertyGenerator) { + $this->addPropertyFromGenerator($property); + } else { + if (is_string($property)) { + $this->addProperty($property); + } elseif (is_array($property)) { + call_user_func_array(array($this, 'addProperty'), $property); + } + } + } + + return $this; + } + + /** + * Add Property from scalars + * + * @param string $name + * @param string|array $defaultValue + * @param int $flags + * @throws Exception\InvalidArgumentException + * @return ClassGenerator + */ + public function addProperty($name, $defaultValue = null, $flags = PropertyGenerator::FLAG_PUBLIC) + { + if (!is_string($name)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects string for name', + __METHOD__ + )); + } + + return $this->addPropertyFromGenerator(new PropertyGenerator($name, $defaultValue, $flags)); + } + + /** + * Add property from PropertyGenerator + * + * @param PropertyGenerator $property + * @throws Exception\InvalidArgumentException + * @return ClassGenerator + */ + public function addPropertyFromGenerator(PropertyGenerator $property) + { + $propertyName = $property->getName(); + + if (isset($this->properties[$propertyName])) { + throw new Exception\InvalidArgumentException(sprintf( + 'A property by name %s already exists in this class.', + $propertyName + )); + } + + $this->properties[$propertyName] = $property; + return $this; + } + + /** + * Add a class to "use" classes + * + * @param string $use + * @param string|null $useAlias + * @return ClassGenerator + */ + public function addUse($use, $useAlias = null) + { + if (!empty($useAlias)) { + $use .= ' as ' . $useAlias; + } + + $this->uses[$use] = $use; + return $this; + } + + /** + * @return PropertyGenerator[] + */ + public function getProperties() + { + return $this->properties; + } + + /** + * @param string $propertyName + * @return PropertyGenerator|false + */ + public function getProperty($propertyName) + { + foreach ($this->getProperties() as $property) { + if ($property->getName() == $propertyName) { + return $property; + } + } + + return false; + } + + /** + * Returns the "use" classes + * + * @return array + */ + public function getUses() + { + return array_values($this->uses); + } + + /** + * @param string $propertyName + * @return bool + */ + public function hasProperty($propertyName) + { + return isset($this->properties[$propertyName]); + } + + /** + * @param array $methods + * @return ClassGenerator + */ + public function addMethods(array $methods) + { + foreach ($methods as $method) { + if ($method instanceof MethodGenerator) { + $this->addMethodFromGenerator($method); + } else { + if (is_string($method)) { + $this->addMethod($method); + } elseif (is_array($method)) { + call_user_func_array(array($this, 'addMethod'), $method); + } + } + } + + return $this; + } + + /** + * Add Method from scalars + * + * @param string $name + * @param array $parameters + * @param int $flags + * @param string $body + * @param string $docBlock + * @throws Exception\InvalidArgumentException + * @return ClassGenerator + */ + public function addMethod( + $name = null, + array $parameters = array(), + $flags = MethodGenerator::FLAG_PUBLIC, + $body = null, + $docBlock = null + ) { + if (!is_string($name)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects string for name', + __METHOD__ + )); + } + + return $this->addMethodFromGenerator(new MethodGenerator($name, $parameters, $flags, $body, $docBlock)); + } + + /** + * Add Method from MethodGenerator + * + * @param MethodGenerator $method + * @throws Exception\InvalidArgumentException + * @return ClassGenerator + */ + public function addMethodFromGenerator(MethodGenerator $method) + { + $methodName = $method->getName(); + + if ($this->hasMethod($methodName)) { + throw new Exception\InvalidArgumentException(sprintf( + 'A method by name %s already exists in this class.', + $methodName + )); + } + + $this->methods[strtolower($methodName)] = $method; + return $this; + } + + /** + * @return MethodGenerator[] + */ + public function getMethods() + { + return $this->methods; + } + + /** + * @param string $methodName + * @return MethodGenerator|false + */ + public function getMethod($methodName) + { + return $this->hasMethod($methodName) ? $this->methods[strtolower($methodName)] : false; + } + + /** + * @param string $methodName + * @return ClassGenerator + */ + public function removeMethod($methodName) + { + if ($this->hasMethod($methodName)) { + unset($this->methods[strtolower($methodName)]); + } + + return $this; + } + + /** + * @param string $methodName + * @return bool + */ + public function hasMethod($methodName) + { + return isset($this->methods[strtolower($methodName)]); + } + + /** + * @return bool + */ + public function isSourceDirty() + { + if (($docBlock = $this->getDocBlock()) && $docBlock->isSourceDirty()) { + return true; + } + + foreach ($this->getProperties() as $property) { + if ($property->isSourceDirty()) { + return true; + } + } + + foreach ($this->getMethods() as $method) { + if ($method->isSourceDirty()) { + return true; + } + } + + return parent::isSourceDirty(); + } + + /** + * @return string + */ + public function generate() + { + if (!$this->isSourceDirty()) { + $output = $this->getSourceContent(); + if (!empty($output)) { + return $output; + } + } + + $output = ''; + + if (null !== ($namespace = $this->getNamespaceName())) { + $output .= 'namespace ' . $namespace . ';' . self::LINE_FEED . self::LINE_FEED; + } + + $uses = $this->getUses(); + if (!empty($uses)) { + foreach ($uses as $use) { + $output .= 'use ' . $use . ';' . self::LINE_FEED; + } + $output .= self::LINE_FEED; + } + + if (null !== ($docBlock = $this->getDocBlock())) { + $docBlock->setIndentation(''); + $output .= $docBlock->generate(); + } + + if ($this->isAbstract()) { + $output .= 'abstract '; + } + + $output .= 'class ' . $this->getName(); + + if (!empty($this->extendedClass)) { + $output .= ' extends ' . $this->extendedClass; + } + + $implemented = $this->getImplementedInterfaces(); + if (!empty($implemented)) { + $output .= ' implements ' . implode(', ', $implemented); + } + + $output .= self::LINE_FEED . '{' . self::LINE_FEED . self::LINE_FEED; + + $properties = $this->getProperties(); + if (!empty($properties)) { + foreach ($properties as $property) { + $output .= $property->generate() . self::LINE_FEED . self::LINE_FEED; + } + } + + $methods = $this->getMethods(); + if (!empty($methods)) { + foreach ($methods as $method) { + $output .= $method->generate() . self::LINE_FEED; + } + } + + $output .= self::LINE_FEED . '}' . self::LINE_FEED; + + return $output; + } +} diff --git a/library/Zend/Code/Generator/DocBlock/Tag.php b/library/Zend/Code/Generator/DocBlock/Tag.php new file mode 100755 index 0000000000..58bd045e0d --- /dev/null +++ b/library/Zend/Code/Generator/DocBlock/Tag.php @@ -0,0 +1,50 @@ +initializeDefaultTags(); + return $tagManager->createTagFromReflection($reflectionTag); + } + + /** + * @param string $description + * @return Tag + * @deprecated Deprecated in 2.3. Use GenericTag::setContent() instead + */ + public function setDescription($description) + { + return $this->setContent($description); + } + + /** + * @return string + * @deprecated Deprecated in 2.3. Use GenericTag::getContent() instead + */ + public function getDescription() + { + return $this->getContent(); + } +} diff --git a/library/Zend/Code/Generator/DocBlock/Tag/AbstractTypeableTag.php b/library/Zend/Code/Generator/DocBlock/Tag/AbstractTypeableTag.php new file mode 100755 index 0000000000..1ecf5dc077 --- /dev/null +++ b/library/Zend/Code/Generator/DocBlock/Tag/AbstractTypeableTag.php @@ -0,0 +1,96 @@ +setTypes($types); + } + + if (!empty($description)) { + $this->setDescription($description); + } + } + + /** + * @param string $description + * @return ReturnTag + */ + public function setDescription($description) + { + $this->description = $description; + return $this; + } + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Array of types or string with types delimited by pipe (|) + * e.g. array('int', 'null') or "int|null" + * + * @param array|string $types + * @return ReturnTag + */ + public function setTypes($types) + { + if (is_string($types)) { + $types = explode('|', $types); + } + $this->types = $types; + return $this; + } + + /** + * @return array + */ + public function getTypes() + { + return $this->types; + } + + /** + * @param string $delimiter + * @return string + */ + public function getTypesAsString($delimiter = '|') + { + return implode($delimiter, $this->types); + } +} diff --git a/library/Zend/Code/Generator/DocBlock/Tag/AuthorTag.php b/library/Zend/Code/Generator/DocBlock/Tag/AuthorTag.php new file mode 100755 index 0000000000..333912dcad --- /dev/null +++ b/library/Zend/Code/Generator/DocBlock/Tag/AuthorTag.php @@ -0,0 +1,110 @@ +setAuthorName($authorName); + } + + if (!empty($authorEmail)) { + $this->setAuthorEmail($authorEmail); + } + } + + /** + * @param ReflectionTagInterface $reflectionTag + * @return ReturnTag + * @deprecated Deprecated in 2.3. Use TagManager::createTagFromReflection() instead + */ + public static function fromReflection(ReflectionTagInterface $reflectionTag) + { + $tagManager = new TagManager(); + $tagManager->initializeDefaultTags(); + return $tagManager->createTagFromReflection($reflectionTag); + } + + /** + * @return string + */ + public function getName() + { + return 'author'; + } + + /** + * @param string $authorEmail + * @return AuthorTag + */ + public function setAuthorEmail($authorEmail) + { + $this->authorEmail = $authorEmail; + return $this; + } + + /** + * @return string + */ + public function getAuthorEmail() + { + return $this->authorEmail; + } + + /** + * @param string $authorName + * @return AuthorTag + */ + public function setAuthorName($authorName) + { + $this->authorName = $authorName; + return $this; + } + + /** + * @return string + */ + public function getAuthorName() + { + return $this->authorName; + } + + /** + * @return string + */ + public function generate() + { + $output = '@author' + . ((!empty($this->authorName)) ? ' ' . $this->authorName : '') + . ((!empty($this->authorEmail)) ? ' <' . $this->authorEmail . '>' : ''); + + return $output; + } +} diff --git a/library/Zend/Code/Generator/DocBlock/Tag/GenericTag.php b/library/Zend/Code/Generator/DocBlock/Tag/GenericTag.php new file mode 100755 index 0000000000..4ff98d9f78 --- /dev/null +++ b/library/Zend/Code/Generator/DocBlock/Tag/GenericTag.php @@ -0,0 +1,88 @@ +setName($name); + } + + if (!empty($content)) { + $this->setContent($content); + } + } + + /** + * @param string $name + * @return GenericTag + */ + public function setName($name) + { + $this->name = ltrim($name, '@'); + return $this; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $content + * @return GenericTag + */ + public function setContent($content) + { + $this->content = $content; + return $this; + } + + /** + * @return string + */ + public function getContent() + { + return $this->content; + } + + /** + * @return string + */ + public function generate() + { + $output = '@' . $this->name + . ((!empty($this->content)) ? ' ' . $this->content : ''); + + return $output; + } +} diff --git a/library/Zend/Code/Generator/DocBlock/Tag/LicenseTag.php b/library/Zend/Code/Generator/DocBlock/Tag/LicenseTag.php new file mode 100755 index 0000000000..91a974152f --- /dev/null +++ b/library/Zend/Code/Generator/DocBlock/Tag/LicenseTag.php @@ -0,0 +1,110 @@ +setUrl($url); + } + + if (!empty($licenseName)) { + $this->setLicenseName($licenseName); + } + } + + /** + * @param ReflectionTagInterface $reflectionTag + * @return ReturnTag + * @deprecated Deprecated in 2.3. Use TagManager::createTagFromReflection() instead + */ + public static function fromReflection(ReflectionTagInterface $reflectionTag) + { + $tagManager = new TagManager(); + $tagManager->initializeDefaultTags(); + return $tagManager->createTagFromReflection($reflectionTag); + } + + /** + * @return string + */ + public function getName() + { + return 'license'; + } + + /** + * @param string $url + * @return LicenseTag + */ + public function setUrl($url) + { + $this->url = $url; + return $this; + } + + /** + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * @param string $name + * @return LicenseTag + */ + public function setLicenseName($name) + { + $this->licenseName = $name; + return $this; + } + + /** + * @return string + */ + public function getLicenseName() + { + return $this->licenseName; + } + + /** + * @return string + */ + public function generate() + { + $output = '@license' + . ((!empty($this->url)) ? ' ' . $this->url : '') + . ((!empty($this->licenseName)) ? ' ' . $this->licenseName : ''); + + return $output; + } +} diff --git a/library/Zend/Code/Generator/DocBlock/Tag/MethodTag.php b/library/Zend/Code/Generator/DocBlock/Tag/MethodTag.php new file mode 100755 index 0000000000..e3c84c4ca6 --- /dev/null +++ b/library/Zend/Code/Generator/DocBlock/Tag/MethodTag.php @@ -0,0 +1,98 @@ +setMethodName($methodName); + } + + $this->setIsStatic((bool) $isStatic); + + parent::__construct($types, $description); + } + + /** + * @return string + */ + public function getName() + { + return 'method'; + } + + /** + * @param boolean $isStatic + * @return MethodTag + */ + public function setIsStatic($isStatic) + { + $this->isStatic = $isStatic; + return $this; + } + + /** + * @return boolean + */ + public function isStatic() + { + return $this->isStatic; + } + + /** + * @param string $methodName + * @return MethodTag + */ + public function setMethodName($methodName) + { + $this->methodName = rtrim($methodName, ')('); + return $this; + } + + /** + * @return string + */ + public function getMethodName() + { + return $this->methodName; + } + + /** + * @return string + */ + public function generate() + { + $output = '@method' + . (($this->isStatic) ? ' static' : '') + . ((!empty($this->types)) ? ' ' . $this->getTypesAsString() : '') + . ((!empty($this->methodName)) ? ' ' . $this->methodName . '()' : '') + . ((!empty($this->description)) ? ' ' . $this->description : ''); + + return $output; + } +} diff --git a/library/Zend/Code/Generator/DocBlock/Tag/ParamTag.php b/library/Zend/Code/Generator/DocBlock/Tag/ParamTag.php new file mode 100755 index 0000000000..75d8f86cb4 --- /dev/null +++ b/library/Zend/Code/Generator/DocBlock/Tag/ParamTag.php @@ -0,0 +1,124 @@ +setVariableName($variableName); + } + + parent::__construct($types, $description); + } + + /** + * @param ReflectionTagInterface $reflectionTag + * @return ReturnTag + * @deprecated Deprecated in 2.3. Use TagManager::createTagFromReflection() instead + */ + public static function fromReflection(ReflectionTagInterface $reflectionTag) + { + $tagManager = new TagManager(); + $tagManager->initializeDefaultTags(); + return $tagManager->createTagFromReflection($reflectionTag); + } + + /** + * @return string + */ + public function getName() + { + return 'param'; + } + + /** + * @param string $variableName + * @return ParamTag + */ + public function setVariableName($variableName) + { + $this->variableName = ltrim($variableName, '$'); + return $this; + } + + /** + * @return string + */ + public function getVariableName() + { + return $this->variableName; + } + + /** + * @param string $datatype + * @return ReturnTag + * @deprecated Deprecated in 2.3. Use setTypes() instead + */ + public function setDatatype($datatype) + { + return $this->setTypes($datatype); + } + + /** + * @return string + * @deprecated Deprecated in 2.3. Use getTypes() or getTypesAsString() instead + */ + public function getDatatype() + { + return $this->getTypesAsString(); + } + + /** + * @param string $paramName + * @return ParamTag + * @deprecated Deprecated in 2.3. Use setVariableName() instead + */ + public function setParamName($paramName) + { + return $this->setVariableName($paramName); + } + + /** + * @return string + * @deprecated Deprecated in 2.3. Use getVariableName() instead + */ + public function getParamName() + { + return $this->getVariableName(); + } + + /** + * @return string + */ + public function generate() + { + $output = '@param' + . ((!empty($this->types)) ? ' ' . $this->getTypesAsString() : '') + . ((!empty($this->variableName)) ? ' $' . $this->variableName : '') + . ((!empty($this->description)) ? ' ' . $this->description : ''); + + return $output; + } +} diff --git a/library/Zend/Code/Generator/DocBlock/Tag/PropertyTag.php b/library/Zend/Code/Generator/DocBlock/Tag/PropertyTag.php new file mode 100755 index 0000000000..26699a8970 --- /dev/null +++ b/library/Zend/Code/Generator/DocBlock/Tag/PropertyTag.php @@ -0,0 +1,71 @@ +setPropertyName($propertyName); + } + + parent::__construct($types, $description); + } + + /** + * @return string + */ + public function getName() + { + return 'property'; + } + + /** + * @param string $propertyName + * @return self + */ + public function setPropertyName($propertyName) + { + $this->propertyName = ltrim($propertyName, '$'); + return $this; + } + + /** + * @return string + */ + public function getPropertyName() + { + return $this->propertyName; + } + + /** + * @return string + */ + public function generate() + { + $output = '@property' + . ((!empty($this->types)) ? ' ' . $this->getTypesAsString() : '') + . ((!empty($this->propertyName)) ? ' $' . $this->propertyName : '') + . ((!empty($this->description)) ? ' ' . $this->description : ''); + + return $output; + } +} diff --git a/library/Zend/Code/Generator/DocBlock/Tag/ReturnTag.php b/library/Zend/Code/Generator/DocBlock/Tag/ReturnTag.php new file mode 100755 index 0000000000..f3b356ebb0 --- /dev/null +++ b/library/Zend/Code/Generator/DocBlock/Tag/ReturnTag.php @@ -0,0 +1,67 @@ +initializeDefaultTags(); + return $tagManager->createTagFromReflection($reflectionTag); + } + + /** + * @return string + */ + public function getName() + { + return 'return'; + } + + /** + * @param string $datatype + * @return ReturnTag + * @deprecated Deprecated in 2.3. Use setTypes() instead + */ + public function setDatatype($datatype) + { + return $this->setTypes($datatype); + } + + /** + * @return string + * @deprecated Deprecated in 2.3. Use getTypes() or getTypesAsString() instead + */ + public function getDatatype() + { + return $this->getTypesAsString(); + } + + /** + * @return string + */ + public function generate() + { + $output = '@return ' + . $this->getTypesAsString() + . ((!empty($this->description)) ? ' ' . $this->description : ''); + + return $output; + } +} diff --git a/library/Zend/Code/Generator/DocBlock/Tag/TagInterface.php b/library/Zend/Code/Generator/DocBlock/Tag/TagInterface.php new file mode 100755 index 0000000000..4d4ef3fcd3 --- /dev/null +++ b/library/Zend/Code/Generator/DocBlock/Tag/TagInterface.php @@ -0,0 +1,16 @@ +types)) ? ' ' . $this->getTypesAsString() : '') + . ((!empty($this->description)) ? ' ' . $this->description : ''); + + return $output; + } +} diff --git a/library/Zend/Code/Generator/DocBlock/TagManager.php b/library/Zend/Code/Generator/DocBlock/TagManager.php new file mode 100755 index 0000000000..4ff3a2bc66 --- /dev/null +++ b/library/Zend/Code/Generator/DocBlock/TagManager.php @@ -0,0 +1,69 @@ +addPrototype(new Tag\ParamTag()); + $this->addPrototype(new Tag\ReturnTag()); + $this->addPrototype(new Tag\MethodTag()); + $this->addPrototype(new Tag\PropertyTag()); + $this->addPrototype(new Tag\AuthorTag()); + $this->addPrototype(new Tag\LicenseTag()); + $this->addPrototype(new Tag\ThrowsTag()); + $this->setGenericPrototype(new Tag\GenericTag()); + } + + /** + * @param ReflectionTagInterface $reflectionTag + * @return TagInterface + */ + public function createTagFromReflection(ReflectionTagInterface $reflectionTag) + { + $tagName = $reflectionTag->getName(); + + /* @var TagInterface $newTag */ + $newTag = $this->getClonedPrototype($tagName); + + // transport any properties via accessors and mutators from reflection to codegen object + $reflectionClass = new \ReflectionClass($reflectionTag); + foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + if (substr($method->getName(), 0, 3) == 'get') { + $propertyName = substr($method->getName(), 3); + if (method_exists($newTag, 'set' . $propertyName)) { + $newTag->{'set' . $propertyName}($reflectionTag->{'get' . $propertyName}()); + } + } elseif (substr($method->getName(), 0, 2) == 'is') { + $propertyName = ucfirst($method->getName()); + if (method_exists($newTag, 'set' . $propertyName)) { + $newTag->{'set' . $propertyName}($reflectionTag->{$method->getName()}()); + } + } + } + return $newTag; + } +} diff --git a/library/Zend/Code/Generator/DocBlockGenerator.php b/library/Zend/Code/Generator/DocBlockGenerator.php new file mode 100755 index 0000000000..e983fa6b1a --- /dev/null +++ b/library/Zend/Code/Generator/DocBlockGenerator.php @@ -0,0 +1,274 @@ +setSourceContent($reflectionDocBlock->getContents()); + $docBlock->setSourceDirty(false); + + $docBlock->setShortDescription($reflectionDocBlock->getShortDescription()); + $docBlock->setLongDescription($reflectionDocBlock->getLongDescription()); + + foreach ($reflectionDocBlock->getTags() as $tag) { + $docBlock->setTag(self::getTagManager()->createTagFromReflection($tag)); + } + + return $docBlock; + } + + /** + * Generate from array + * + * @configkey shortdescription string The short description for this doc block + * @configkey longdescription string The long description for this doc block + * @configkey tags array + * + * @throws Exception\InvalidArgumentException + * @param array $array + * @return DocBlockGenerator + */ + public static function fromArray(array $array) + { + $docBlock = new static(); + + foreach ($array as $name => $value) { + // normalize key + switch (strtolower(str_replace(array('.', '-', '_'), '', $name))) { + case 'shortdescription': + $docBlock->setShortDescription($value); + break; + case 'longdescription': + $docBlock->setLongDescription($value); + break; + case 'tags': + $docBlock->setTags($value); + break; + } + } + + return $docBlock; + } + + protected static function getTagManager() + { + if (!isset(static::$tagManager)) { + static::$tagManager = new TagManager(); + static::$tagManager->initializeDefaultTags(); + } + return static::$tagManager; + } + + /** + * @param string $shortDescription + * @param string $longDescription + * @param array $tags + */ + public function __construct($shortDescription = null, $longDescription = null, array $tags = array()) + { + if ($shortDescription) { + $this->setShortDescription($shortDescription); + } + if ($longDescription) { + $this->setLongDescription($longDescription); + } + if (is_array($tags) && $tags) { + $this->setTags($tags); + } + } + + /** + * @param string $shortDescription + * @return DocBlockGenerator + */ + public function setShortDescription($shortDescription) + { + $this->shortDescription = $shortDescription; + return $this; + } + + /** + * @return string + */ + public function getShortDescription() + { + return $this->shortDescription; + } + + /** + * @param string $longDescription + * @return DocBlockGenerator + */ + public function setLongDescription($longDescription) + { + $this->longDescription = $longDescription; + return $this; + } + + /** + * @return string + */ + public function getLongDescription() + { + return $this->longDescription; + } + + /** + * @param array $tags + * @return DocBlockGenerator + */ + public function setTags(array $tags) + { + foreach ($tags as $tag) { + $this->setTag($tag); + } + + return $this; + } + + /** + * @param array|TagInterface $tag + * @throws Exception\InvalidArgumentException + * @return DocBlockGenerator + */ + public function setTag($tag) + { + if (is_array($tag)) { + // use deprecated Tag class for backward compatiblity to old array-keys + $genericTag = new Tag(); + $genericTag->setOptions($tag); + $tag = $genericTag; + } elseif (!$tag instanceof TagInterface) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects either an array of method options or an instance of %s\DocBlock\Tag\TagInterface', + __METHOD__, + __NAMESPACE__ + )); + } + + $this->tags[] = $tag; + return $this; + } + + /** + * @return TagInterface[] + */ + public function getTags() + { + return $this->tags; + } + + /** + * @param bool $value + * @return DocBlockGenerator + */ + public function setWordWrap($value) + { + $this->wordwrap = (bool) $value; + return $this; + } + + /** + * @return bool + */ + public function getWordWrap() + { + return $this->wordwrap; + } + + /** + * @return string + */ + public function generate() + { + if (!$this->isSourceDirty()) { + return $this->docCommentize(trim($this->getSourceContent())); + } + + $output = ''; + if (null !== ($sd = $this->getShortDescription())) { + $output .= $sd . self::LINE_FEED . self::LINE_FEED; + } + if (null !== ($ld = $this->getLongDescription())) { + $output .= $ld . self::LINE_FEED . self::LINE_FEED; + } + + /* @var $tag GeneratorInterface */ + foreach ($this->getTags() as $tag) { + $output .= $tag->generate() . self::LINE_FEED; + } + + return $this->docCommentize(trim($output)); + } + + /** + * @param string $content + * @return string + */ + protected function docCommentize($content) + { + $indent = $this->getIndentation(); + $output = $indent . '/**' . self::LINE_FEED; + $content = $this->getWordWrap() == true ? wordwrap($content, 80, self::LINE_FEED) : $content; + $lines = explode(self::LINE_FEED, $content); + foreach ($lines as $line) { + $output .= $indent . ' *'; + if ($line) { + $output .= " $line"; + } + $output .= self::LINE_FEED; + } + $output .= $indent . ' */' . self::LINE_FEED; + + return $output; + } +} diff --git a/library/Zend/Code/Generator/Exception/ExceptionInterface.php b/library/Zend/Code/Generator/Exception/ExceptionInterface.php new file mode 100755 index 0000000000..d6abfd426e --- /dev/null +++ b/library/Zend/Code/Generator/Exception/ExceptionInterface.php @@ -0,0 +1,16 @@ +setOptions($options); + } + } + + /** + * Use this if you intend on generating code generation objects based on the same file. + * This will keep previous changes to the file in tact during the same PHP process + * + * @param string $filePath + * @param bool $includeIfNotAlreadyIncluded + * @throws ReflectionException\InvalidArgumentException If file does not exists + * @throws ReflectionException\RuntimeException If file exists but is not included or required + * @return FileGenerator + */ + public static function fromReflectedFileName($filePath, $includeIfNotAlreadyIncluded = true) + { + $fileReflector = new FileReflection($filePath, $includeIfNotAlreadyIncluded); + $codeGenerator = static::fromReflection($fileReflector); + + return $codeGenerator; + } + + /** + * @param FileReflection $fileReflection + * @return FileGenerator + */ + public static function fromReflection(FileReflection $fileReflection) + { + $file = new static(); + + $file->setSourceContent($fileReflection->getContents()); + $file->setSourceDirty(false); + + $body = $fileReflection->getContents(); + + $uses = $fileReflection->getUses(); + + foreach ($fileReflection->getClasses() as $class) { + $phpClass = ClassGenerator::fromReflection($class); + $phpClass->setContainingFileGenerator($file); + + foreach ($uses as $fileUse) { + $phpClass->addUse($fileUse['use'], $fileUse['as']); + } + + $file->setClass($phpClass); + } + + $namespace = $fileReflection->getNamespace(); + + if ($namespace != '') { + $file->setNamespace($namespace); + } + + if ($uses) { + $file->setUses($uses); + } + + if (($fileReflection->getDocComment() != '')) { + $docBlock = $fileReflection->getDocBlock(); + $file->setDocBlock(DocBlockGenerator::fromReflection($docBlock)); + } + + return $file; + } + + /** + * @param array $values + * @return FileGenerator + */ + public static function fromArray(array $values) + { + $fileGenerator = new static; + foreach ($values as $name => $value) { + switch (strtolower(str_replace(array('.', '-', '_'), '', $name))) { + case 'filename': + $fileGenerator->setFilename($value); + continue; + case 'class': + $fileGenerator->setClass(($value instanceof ClassGenerator) ? $value : ClassGenerator::fromArray($value)); + continue; + case 'requiredfiles': + $fileGenerator->setRequiredFiles($value); + continue; + default: + if (property_exists($fileGenerator, $name)) { + $fileGenerator->{$name} = $value; + } elseif (method_exists($fileGenerator, 'set' . $name)) { + $fileGenerator->{'set' . $name}($value); + } + } + } + + return $fileGenerator; + } + + /** + * @param DocBlockGenerator|string $docBlock + * @throws Exception\InvalidArgumentException + * @return FileGenerator + */ + public function setDocBlock($docBlock) + { + if (is_string($docBlock)) { + $docBlock = array('shortDescription' => $docBlock); + } + + if (is_array($docBlock)) { + $docBlock = new DocBlockGenerator($docBlock); + } elseif (!$docBlock instanceof DocBlockGenerator) { + throw new Exception\InvalidArgumentException(sprintf( + '%s is expecting either a string, array or an instance of %s\DocBlockGenerator', + __METHOD__, + __NAMESPACE__ + )); + } + + $this->docBlock = $docBlock; + return $this; + } + + /** + * @return DocBlockGenerator + */ + public function getDocBlock() + { + return $this->docBlock; + } + + /** + * @param array $requiredFiles + * @return FileGenerator + */ + public function setRequiredFiles(array $requiredFiles) + { + $this->requiredFiles = $requiredFiles; + return $this; + } + + /** + * @return array + */ + public function getRequiredFiles() + { + return $this->requiredFiles; + } + + /** + * @return string + */ + public function getNamespace() + { + return $this->namespace; + } + + /** + * @param string $namespace + * @return FileGenerator + */ + public function setNamespace($namespace) + { + $this->namespace = (string) $namespace; + return $this; + } + + /** + * Returns an array with the first element the use statement, second is the as part. + * If $withResolvedAs is set to true, there will be a third element that is the + * "resolved" as statement, as the second part is not required in use statements + * + * @param bool $withResolvedAs + * @return array + */ + public function getUses($withResolvedAs = false) + { + $uses = $this->uses; + if ($withResolvedAs) { + for ($useIndex = 0, $count = count($uses); $useIndex < $count; $useIndex++) { + if ($uses[$useIndex][1] == '') { + if (($lastSeparator = strrpos($uses[$useIndex][0], '\\')) !== false) { + $uses[$useIndex][2] = substr($uses[$useIndex][0], $lastSeparator + 1); + } else { + $uses[$useIndex][2] = $uses[$useIndex][0]; + } + } else { + $uses[$useIndex][2] = $uses[$useIndex][1]; + } + } + } + + return $uses; + } + + /** + * @param array $uses + * @return FileGenerator + */ + public function setUses(array $uses) + { + foreach ($uses as $use) { + $use = (array) $use; + if (array_key_exists('use', $use) && array_key_exists('as', $use)) { + $import = $use['use']; + $alias = $use['as']; + } elseif (count($use) == 2) { + list($import, $alias) = $use; + } else { + $import = current($use); + $alias = null; + } + $this->setUse($import, $alias); + } + return $this; + } + + /** + * @param string $use + * @param null|string $as + * @return FileGenerator + */ + public function setUse($use, $as = null) + { + if (!in_array(array($use, $as), $this->uses)) { + $this->uses[] = array($use, $as); + } + return $this; + } + + /** + * @param array $classes + * @return FileGenerator + */ + public function setClasses(array $classes) + { + foreach ($classes as $class) { + $this->setClass($class); + } + + return $this; + } + + /** + * @param string $name + * @return ClassGenerator + */ + public function getClass($name = null) + { + if ($name == null) { + reset($this->classes); + + return current($this->classes); + } + + return $this->classes[(string) $name]; + } + + /** + * @param array|string|ClassGenerator $class + * @throws Exception\InvalidArgumentException + * @return FileGenerator + */ + public function setClass($class) + { + if (is_array($class)) { + $class = ClassGenerator::fromArray($class); + } elseif (is_string($class)) { + $class = new ClassGenerator($class); + } elseif (!$class instanceof ClassGenerator) { + throw new Exception\InvalidArgumentException(sprintf( + '%s is expecting either a string, array or an instance of %s\ClassGenerator', + __METHOD__, + __NAMESPACE__ + )); + } + + // @todo check for dup here + $className = $class->getName(); + $this->classes[$className] = $class; + + return $this; + } + + /** + * @param string $filename + * @return FileGenerator + */ + public function setFilename($filename) + { + $this->filename = (string) $filename; + return $this; + } + + /** + * @return string + */ + public function getFilename() + { + return $this->filename; + } + + /** + * @return ClassGenerator[] + */ + public function getClasses() + { + return $this->classes; + } + + /** + * @param string $body + * @return FileGenerator + */ + public function setBody($body) + { + $this->body = (string) $body; + return $this; + } + + /** + * @return string + */ + public function getBody() + { + return $this->body; + } + + /** + * @return bool + */ + public function isSourceDirty() + { + $docBlock = $this->getDocBlock(); + if ($docBlock && $docBlock->isSourceDirty()) { + return true; + } + + foreach ($this->classes as $class) { + if ($class->isSourceDirty()) { + return true; + } + } + + return parent::isSourceDirty(); + } + + /** + * @return string + */ + public function generate() + { + if ($this->isSourceDirty() === false) { + return $this->sourceContent; + } + + $output = ''; + + // @note body gets populated when FileGenerator created + // from a file. @see fromReflection and may also be set + // via FileGenerator::setBody + $body = $this->getBody(); + + // start with the body (if there), or open tag + if (preg_match('#(?:\s*)<\?php#', $body) == false) { + $output = 'getDocBlock())) { + $docBlock->setIndentation(''); + + if (preg_match('#/\* Zend_Code_Generator_FileGenerator-DocBlockMarker \*/#m', $output)) { + $output = preg_replace('#/\* Zend_Code_Generator_FileGenerator-DocBlockMarker \*/#m', $docBlock->generate(), $output, 1); + } else { + $output .= $docBlock->generate() . self::LINE_FEED; + } + } + + // newline + $output .= self::LINE_FEED; + + // namespace, if any + $namespace = $this->getNamespace(); + if ($namespace) { + $namespace = sprintf('namespace %s;%s', $namespace, str_repeat(self::LINE_FEED, 2)); + if (preg_match('#/\* Zend_Code_Generator_FileGenerator-NamespaceMarker \*/#m', $output)) { + $output = preg_replace('#/\* Zend_Code_Generator_FileGenerator-NamespaceMarker \*/#m', $namespace, + $output, 1); + } else { + $output .= $namespace; + } + } + + // process required files + // @todo marker replacement for required files + $requiredFiles = $this->getRequiredFiles(); + if (!empty($requiredFiles)) { + foreach ($requiredFiles as $requiredFile) { + $output .= 'require_once \'' . $requiredFile . '\';' . self::LINE_FEED; + } + + $output .= self::LINE_FEED; + } + + $classes = $this->getClasses(); + $classUses = array(); + //build uses array + foreach ($classes as $class) { + //check for duplicate use statements + $uses = $class->getUses(); + if (!empty($uses) && is_array($uses)) { + $classUses = array_merge($classUses, $uses); + } + } + + // process import statements + $uses = $this->getUses(); + if (!empty($uses)) { + $useOutput = ''; + + foreach ($uses as $use) { + list($import, $alias) = $use; + if (null === $alias) { + $tempOutput = sprintf('%s', $import); + } else { + $tempOutput = sprintf('%s as %s', $import, $alias); + } + + //don't duplicate use statements + if (!in_array($tempOutput, $classUses)) { + $useOutput .= "use ". $tempOutput .";"; + $useOutput .= self::LINE_FEED; + } + } + $useOutput .= self::LINE_FEED; + + if (preg_match('#/\* Zend_Code_Generator_FileGenerator-UseMarker \*/#m', $output)) { + $output = preg_replace('#/\* Zend_Code_Generator_FileGenerator-UseMarker \*/#m', $useOutput, + $output, 1); + } else { + $output .= $useOutput; + } + } + + // process classes + if (!empty($classes)) { + foreach ($classes as $class) { + $regex = str_replace('&', $class->getName(), '/\* Zend_Code_Generator_Php_File-ClassMarker: \{[A-Za-z0-9\\\]+?&\} \*/'); + if (preg_match('#' . $regex . '#m', $output)) { + $output = preg_replace('#' . $regex . '#', $class->generate(), $output, 1); + } else { + if ($namespace) { + $class->setNamespaceName(null); + } + $output .= $class->generate() . self::LINE_FEED; + } + } + } + + if (!empty($body)) { + // add an extra space between classes and + if (!empty($classes)) { + $output .= self::LINE_FEED; + } + + $output .= $body; + } + + return $output; + } + + /** + * @return FileGenerator + * @throws Exception\RuntimeException + */ + public function write() + { + if ($this->filename == '' || !is_writable(dirname($this->filename))) { + throw new Exception\RuntimeException('This code generator object is not writable.'); + } + file_put_contents($this->filename, $this->generate()); + + return $this; + } +} diff --git a/library/Zend/Code/Generator/FileGeneratorRegistry.php b/library/Zend/Code/Generator/FileGeneratorRegistry.php new file mode 100755 index 0000000000..164cc52543 --- /dev/null +++ b/library/Zend/Code/Generator/FileGeneratorRegistry.php @@ -0,0 +1,44 @@ +getFilename(); + } + + if ($fileName == '') { + throw new RuntimeException('FileName does not exist.'); + } + + // cannot use realpath since the file might not exist, but we do need to have the index + // in the same DIRECTORY_SEPARATOR that realpath would use: + $fileName = str_replace(array('\\', '/'), DIRECTORY_SEPARATOR, $fileName); + + static::$fileCodeGenerators[$fileName] = $fileCodeGenerator; + } +} diff --git a/library/Zend/Code/Generator/GeneratorInterface.php b/library/Zend/Code/Generator/GeneratorInterface.php new file mode 100755 index 0000000000..77e1eb8c80 --- /dev/null +++ b/library/Zend/Code/Generator/GeneratorInterface.php @@ -0,0 +1,15 @@ +setSourceContent($reflectionMethod->getContents(false)); + $method->setSourceDirty(false); + + if ($reflectionMethod->getDocComment() != '') { + $method->setDocBlock(DocBlockGenerator::fromReflection($reflectionMethod->getDocBlock())); + } + + $method->setFinal($reflectionMethod->isFinal()); + + if ($reflectionMethod->isPrivate()) { + $method->setVisibility(self::VISIBILITY_PRIVATE); + } elseif ($reflectionMethod->isProtected()) { + $method->setVisibility(self::VISIBILITY_PROTECTED); + } else { + $method->setVisibility(self::VISIBILITY_PUBLIC); + } + + $method->setStatic($reflectionMethod->isStatic()); + + $method->setName($reflectionMethod->getName()); + + foreach ($reflectionMethod->getParameters() as $reflectionParameter) { + $method->setParameter(ParameterGenerator::fromReflection($reflectionParameter)); + } + + $method->setBody($reflectionMethod->getBody()); + + return $method; + } + + /** + * Generate from array + * + * @configkey name string [required] Class Name + * @configkey docblock string The docblock information + * @configkey flags int Flags, one of MethodGenerator::FLAG_ABSTRACT MethodGenerator::FLAG_FINAL + * @configkey parameters string Class which this class is extending + * @configkey body string + * @configkey abstract bool + * @configkey final bool + * @configkey static bool + * @configkey visibility string + * + * @throws Exception\InvalidArgumentException + * @param array $array + * @return MethodGenerator + */ + public static function fromArray(array $array) + { + if (!isset($array['name'])) { + throw new Exception\InvalidArgumentException( + 'Method generator requires that a name is provided for this object' + ); + } + + $method = new static($array['name']); + foreach ($array as $name => $value) { + // normalize key + switch (strtolower(str_replace(array('.', '-', '_'), '', $name))) { + case 'docblock': + $docBlock = ($value instanceof DocBlockGenerator) ? $value : DocBlockGenerator::fromArray($value); + $method->setDocBlock($docBlock); + break; + case 'flags': + $method->setFlags($value); + break; + case 'parameters': + $method->setParameters($value); + break; + case 'body': + $method->setBody($value); + break; + case 'abstract': + $method->setAbstract($value); + break; + case 'final': + $method->setFinal($value); + break; + case 'static': + $method->setStatic($value); + break; + case 'visibility': + $method->setVisibility($value); + break; + } + } + + return $method; + } + + /** + * @param string $name + * @param array $parameters + * @param int $flags + * @param string $body + * @param DocBlockGenerator|string $docBlock + */ + public function __construct( + $name = null, + array $parameters = array(), + $flags = self::FLAG_PUBLIC, + $body = null, + $docBlock = null + ) { + if ($name) { + $this->setName($name); + } + if ($parameters) { + $this->setParameters($parameters); + } + if ($flags !== self::FLAG_PUBLIC) { + $this->setFlags($flags); + } + if ($body) { + $this->setBody($body); + } + if ($docBlock) { + $this->setDocBlock($docBlock); + } + } + + /** + * @param array $parameters + * @return MethodGenerator + */ + public function setParameters(array $parameters) + { + foreach ($parameters as $parameter) { + $this->setParameter($parameter); + } + + return $this; + } + + /** + * @param ParameterGenerator|string $parameter + * @throws Exception\InvalidArgumentException + * @return MethodGenerator + */ + public function setParameter($parameter) + { + if (is_string($parameter)) { + $parameter = new ParameterGenerator($parameter); + } elseif (!$parameter instanceof ParameterGenerator) { + throw new Exception\InvalidArgumentException(sprintf( + '%s is expecting either a string, array or an instance of %s\ParameterGenerator', + __METHOD__, + __NAMESPACE__ + )); + } + + $parameterName = $parameter->getName(); + + $this->parameters[$parameterName] = $parameter; + + return $this; + } + + /** + * @return ParameterGenerator[] + */ + public function getParameters() + { + return $this->parameters; + } + + /** + * @param string $body + * @return MethodGenerator + */ + public function setBody($body) + { + $this->body = $body; + return $this; + } + + /** + * @return string + */ + public function getBody() + { + return $this->body; + } + + /** + * @return string + */ + public function generate() + { + $output = ''; + + $indent = $this->getIndentation(); + + if (($docBlock = $this->getDocBlock()) !== null) { + $docBlock->setIndentation($indent); + $output .= $docBlock->generate(); + } + + $output .= $indent; + + if ($this->isAbstract()) { + $output .= 'abstract '; + } else { + $output .= (($this->isFinal()) ? 'final ' : ''); + } + + $output .= $this->getVisibility() + . (($this->isStatic()) ? ' static' : '') + . ' function ' . $this->getName() . '('; + + $parameters = $this->getParameters(); + if (!empty($parameters)) { + foreach ($parameters as $parameter) { + $parameterOutput[] = $parameter->generate(); + } + + $output .= implode(', ', $parameterOutput); + } + + $output .= ')'; + + if ($this->isAbstract()) { + return $output . ';'; + } + + $output .= self::LINE_FEED . $indent . '{' . self::LINE_FEED; + + if ($this->body) { + $output .= preg_replace('#^((?![a-zA-Z0-9_-]+;).+?)$#m', $indent . $indent . '$1', trim($this->body)) + . self::LINE_FEED; + } + + $output .= $indent . '}' . self::LINE_FEED; + + return $output; + } + + public function __toString() + { + return $this->generate(); + } +} diff --git a/library/Zend/Code/Generator/ParameterGenerator.php b/library/Zend/Code/Generator/ParameterGenerator.php new file mode 100755 index 0000000000..30fad22c09 --- /dev/null +++ b/library/Zend/Code/Generator/ParameterGenerator.php @@ -0,0 +1,300 @@ +setName($reflectionParameter->getName()); + + if ($reflectionParameter->isArray()) { + $param->setType('array'); + } elseif (method_exists($reflectionParameter, 'isCallable') && $reflectionParameter->isCallable()) { + $param->setType('callable'); + } else { + $typeClass = $reflectionParameter->getClass(); + if ($typeClass) { + $parameterType = $typeClass->getName(); + $currentNamespace = $reflectionParameter->getDeclaringClass()->getNamespaceName(); + + if (!empty($currentNamespace) && substr($parameterType, 0, strlen($currentNamespace)) == $currentNamespace) { + $parameterType = substr($parameterType, strlen($currentNamespace) + 1); + } else { + $parameterType = '\\' . trim($parameterType, '\\'); + } + + $param->setType($parameterType); + } + } + + $param->setPosition($reflectionParameter->getPosition()); + + if ($reflectionParameter->isOptional()) { + $param->setDefaultValue($reflectionParameter->getDefaultValue()); + } + $param->setPassedByReference($reflectionParameter->isPassedByReference()); + + return $param; + } + + /** + * Generate from array + * + * @configkey name string [required] Class Name + * @configkey type string + * @configkey defaultvalue null|bool|string|int|float|array|ValueGenerator + * @configkey passedbyreference bool + * @configkey position int + * @configkey sourcedirty bool + * @configkey indentation string + * @configkey sourcecontent string + * + * @throws Exception\InvalidArgumentException + * @param array $array + * @return ParameterGenerator + */ + public static function fromArray(array $array) + { + if (!isset($array['name'])) { + throw new Exception\InvalidArgumentException( + 'Paramerer generator requires that a name is provided for this object' + ); + } + + $param = new static($array['name']); + foreach ($array as $name => $value) { + // normalize key + switch (strtolower(str_replace(array('.', '-', '_'), '', $name))) { + case 'type': + $param->setType($value); + break; + case 'defaultvalue': + $param->setDefaultValue($value); + break; + case 'passedbyreference': + $param->setPassedByReference($value); + break; + case 'position': + $param->setPosition($value); + break; + case 'sourcedirty': + $param->setSourceDirty($value); + break; + case 'indentation': + $param->setIndentation($value); + break; + case 'sourcecontent': + $param->setSourceContent($value); + break; + } + } + + return $param; + } + + /** + * @param string $name + * @param string $type + * @param mixed $defaultValue + * @param int $position + * @param bool $passByReference + */ + public function __construct( + $name = null, + $type = null, + $defaultValue = null, + $position = null, + $passByReference = false + ) { + if (null !== $name) { + $this->setName($name); + } + if (null !== $type) { + $this->setType($type); + } + if (null !== $defaultValue) { + $this->setDefaultValue($defaultValue); + } + if (null !== $position) { + $this->setPosition($position); + } + if (false !== $passByReference) { + $this->setPassedByReference(true); + } + } + + /** + * @param string $type + * @return ParameterGenerator + */ + public function setType($type) + { + $this->type = (string) $type; + return $this; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * @param string $name + * @return ParameterGenerator + */ + public function setName($name) + { + $this->name = (string) $name; + return $this; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set the default value of the parameter. + * + * Certain variables are difficult to express + * + * @param null|bool|string|int|float|array|ValueGenerator $defaultValue + * @return ParameterGenerator + */ + public function setDefaultValue($defaultValue) + { + if (!($defaultValue instanceof ValueGenerator)) { + $defaultValue = new ValueGenerator($defaultValue); + } + $this->defaultValue = $defaultValue; + + return $this; + } + + /** + * @return string + */ + public function getDefaultValue() + { + return $this->defaultValue; + } + + /** + * @param int $position + * @return ParameterGenerator + */ + public function setPosition($position) + { + $this->position = (int) $position; + return $this; + } + + /** + * @return int + */ + public function getPosition() + { + return $this->position; + } + + /** + * @return bool + */ + public function getPassedByReference() + { + return $this->passedByReference; + } + + /** + * @param bool $passedByReference + * @return ParameterGenerator + */ + public function setPassedByReference($passedByReference) + { + $this->passedByReference = (bool) $passedByReference; + return $this; + } + + /** + * @return string + */ + public function generate() + { + $output = ''; + + if ($this->type && !in_array($this->type, static::$simple)) { + $output .= $this->type . ' '; + } + + if (true === $this->passedByReference) { + $output .= '&'; + } + + $output .= '$' . $this->name; + + if ($this->defaultValue !== null) { + $output .= ' = '; + if (is_string($this->defaultValue)) { + $output .= ValueGenerator::escape($this->defaultValue); + } elseif ($this->defaultValue instanceof ValueGenerator) { + $this->defaultValue->setOutputMode(ValueGenerator::OUTPUT_SINGLE_LINE); + $output .= $this->defaultValue; + } else { + $output .= $this->defaultValue; + } + } + + return $output; + } +} diff --git a/library/Zend/Code/Generator/PropertyGenerator.php b/library/Zend/Code/Generator/PropertyGenerator.php new file mode 100755 index 0000000000..36735ad37f --- /dev/null +++ b/library/Zend/Code/Generator/PropertyGenerator.php @@ -0,0 +1,226 @@ +setName($reflectionProperty->getName()); + + $allDefaultProperties = $reflectionProperty->getDeclaringClass()->getDefaultProperties(); + + $property->setDefaultValue($allDefaultProperties[$reflectionProperty->getName()]); + + if ($reflectionProperty->getDocComment() != '') { + $property->setDocBlock(DocBlockGenerator::fromReflection($reflectionProperty->getDocBlock())); + } + + if ($reflectionProperty->isStatic()) { + $property->setStatic(true); + } + + if ($reflectionProperty->isPrivate()) { + $property->setVisibility(self::VISIBILITY_PRIVATE); + } elseif ($reflectionProperty->isProtected()) { + $property->setVisibility(self::VISIBILITY_PROTECTED); + } else { + $property->setVisibility(self::VISIBILITY_PUBLIC); + } + + $property->setSourceDirty(false); + + return $property; + } + + /** + * Generate from array + * + * @configkey name string [required] Class Name + * @configkey const bool + * @configkey defaultvalue null|bool|string|int|float|array|ValueGenerator + * @configkey flags int + * @configkey abstract bool + * @configkey final bool + * @configkey static bool + * @configkey visibility string + * + * @throws Exception\InvalidArgumentException + * @param array $array + * @return PropertyGenerator + */ + public static function fromArray(array $array) + { + if (!isset($array['name'])) { + throw new Exception\InvalidArgumentException( + 'Property generator requires that a name is provided for this object' + ); + } + + $property = new static($array['name']); + foreach ($array as $name => $value) { + // normalize key + switch (strtolower(str_replace(array('.', '-', '_'), '', $name))) { + case 'const': + $property->setConst($value); + break; + case 'defaultvalue': + $property->setDefaultValue($value); + break; + case 'docblock': + $docBlock = ($value instanceof DocBlockGenerator) ? $value : DocBlockGenerator::fromArray($value); + $property->setDocBlock($docBlock); + break; + case 'flags': + $property->setFlags($value); + break; + case 'abstract': + $property->setAbstract($value); + break; + case 'final': + $property->setFinal($value); + break; + case 'static': + $property->setStatic($value); + break; + case 'visibility': + $property->setVisibility($value); + break; + } + } + + return $property; + } + + /** + * @param string $name + * @param PropertyValueGenerator|string|array $defaultValue + * @param int $flags + */ + public function __construct($name = null, $defaultValue = null, $flags = self::FLAG_PUBLIC) + { + if (null !== $name) { + $this->setName($name); + } + if (null !== $defaultValue) { + $this->setDefaultValue($defaultValue); + } + if ($flags !== self::FLAG_PUBLIC) { + $this->setFlags($flags); + } + } + + /** + * @param bool $const + * @return PropertyGenerator + */ + public function setConst($const) + { + if ($const) { + $this->removeFlag(self::FLAG_PUBLIC | self::FLAG_PRIVATE | self::FLAG_PROTECTED); + $this->setFlags(self::FLAG_CONSTANT); + } else { + $this->removeFlag(self::FLAG_CONSTANT); + } + + return $this; + } + + /** + * @return bool + */ + public function isConst() + { + return (bool) ($this->flags & self::FLAG_CONSTANT); + } + + /** + * @param PropertyValueGenerator|mixed $defaultValue + * @param string $defaultValueType + * @param string $defaultValueOutputMode + * + * @return PropertyGenerator + */ + public function setDefaultValue($defaultValue, $defaultValueType = PropertyValueGenerator::TYPE_AUTO, $defaultValueOutputMode = PropertyValueGenerator::OUTPUT_MULTIPLE_LINE) + { + if (!($defaultValue instanceof PropertyValueGenerator)) { + $defaultValue = new PropertyValueGenerator($defaultValue, $defaultValueType, $defaultValueOutputMode); + } + + $this->defaultValue = $defaultValue; + + return $this; + } + + /** + * @return PropertyValueGenerator + */ + public function getDefaultValue() + { + return $this->defaultValue; + } + + /** + * @throws Exception\RuntimeException + * @return string + */ + public function generate() + { + $name = $this->getName(); + $defaultValue = $this->getDefaultValue(); + + $output = ''; + + if (($docBlock = $this->getDocBlock()) !== null) { + $docBlock->setIndentation(' '); + $output .= $docBlock->generate(); + } + + if ($this->isConst()) { + if ($defaultValue != null && !$defaultValue->isValidConstantType()) { + throw new Exception\RuntimeException(sprintf( + 'The property %s is said to be ' + . 'constant but does not have a valid constant value.', + $this->name + )); + } + $output .= $this->indentation . 'const ' . $name . ' = ' + . (($defaultValue !== null) ? $defaultValue->generate() : 'null;'); + } else { + $output .= $this->indentation + . $this->getVisibility() + . (($this->isStatic()) ? ' static' : '') + . ' $' . $name . ' = ' + . (($defaultValue !== null) ? $defaultValue->generate() : 'null;'); + } + + return $output; + } +} diff --git a/library/Zend/Code/Generator/PropertyValueGenerator.php b/library/Zend/Code/Generator/PropertyValueGenerator.php new file mode 100755 index 0000000000..f36fc8c1fc --- /dev/null +++ b/library/Zend/Code/Generator/PropertyValueGenerator.php @@ -0,0 +1,23 @@ +setValue($value); + } + if ($type !== self::TYPE_AUTO) { + $this->setType($type); + } + if ($outputMode !== self::OUTPUT_MULTIPLE_LINE) { + $this->setOutputMode($outputMode); + } + if ($constants !== null) { + $this->constants = $constants; + } else { + $this->constants = new ArrayObject(); + } + } + + /** + * Init constant list by defined and magic constants + */ + public function initEnvironmentConstants() + { + $constants = array( + '__DIR__', + '__FILE__', + '__LINE__', + '__CLASS__', + '__TRAIT__', + '__METHOD__', + '__FUNCTION__', + '__NAMESPACE__', + '::' + ); + $constants = array_merge($constants, array_keys(get_defined_constants()), $this->constants->getArrayCopy()); + $this->constants->exchangeArray($constants); + } + + /** + * Add constant to list + * + * @param string $constant + * + * @return $this + */ + public function addConstant($constant) + { + $this->constants->append($constant); + + return $this; + } + + /** + * Delete constant from constant list + * + * @param string $constant + * + * @return bool + */ + public function deleteConstant($constant) + { + if (($index = array_search($constant, $this->constants->getArrayCopy())) !== false) { + $this->constants->offsetUnset($index); + } + + return $index !== false; + } + + /** + * Return constant list + * + * @return ArrayObject + */ + public function getConstants() + { + return $this->constants; + } + + /** + * @return bool + */ + public function isValidConstantType() + { + if ($this->type == self::TYPE_AUTO) { + $type = $this->getAutoDeterminedType($this->value); + } else { + $type = $this->type; + } + + // valid types for constants + $scalarTypes = array( + self::TYPE_BOOLEAN, + self::TYPE_BOOL, + self::TYPE_NUMBER, + self::TYPE_INTEGER, + self::TYPE_INT, + self::TYPE_FLOAT, + self::TYPE_DOUBLE, + self::TYPE_STRING, + self::TYPE_CONSTANT, + self::TYPE_NULL + ); + + return in_array($type, $scalarTypes); + } + + /** + * @param mixed $value + * @return ValueGenerator + */ + public function setValue($value) + { + $this->value = $value; + return $this; + } + + /** + * @return mixed + */ + public function getValue() + { + return $this->value; + } + + /** + * @param string $type + * @return ValueGenerator + */ + public function setType($type) + { + $this->type = (string) $type; + return $this; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * @param int $arrayDepth + * @return ValueGenerator + */ + public function setArrayDepth($arrayDepth) + { + $this->arrayDepth = (int) $arrayDepth; + return $this; + } + + /** + * @return int + */ + public function getArrayDepth() + { + return $this->arrayDepth; + } + + /** + * @param string $type + * @return string + */ + protected function getValidatedType($type) + { + $types = array( + self::TYPE_AUTO, + self::TYPE_BOOLEAN, + self::TYPE_BOOL, + self::TYPE_NUMBER, + self::TYPE_INTEGER, + self::TYPE_INT, + self::TYPE_FLOAT, + self::TYPE_DOUBLE, + self::TYPE_STRING, + self::TYPE_ARRAY, + self::TYPE_CONSTANT, + self::TYPE_NULL, + self::TYPE_OBJECT, + self::TYPE_OTHER + ); + + if (in_array($type, $types)) { + return $type; + } + + return self::TYPE_AUTO; + } + + /** + * @param mixed $value + * @return string + */ + public function getAutoDeterminedType($value) + { + switch (gettype($value)) { + case 'boolean': + return self::TYPE_BOOLEAN; + case 'string': + foreach ($this->constants as $constant) { + if (strpos($value, $constant) !== false) { + return self::TYPE_CONSTANT; + } + } + return self::TYPE_STRING; + case 'double': + case 'float': + case 'integer': + return self::TYPE_NUMBER; + case 'array': + return self::TYPE_ARRAY; + case 'NULL': + return self::TYPE_NULL; + case 'object': + case 'resource': + case 'unknown type': + default: + return self::TYPE_OTHER; + } + } + + /** + * @throws Exception\RuntimeException + * @return string + */ + public function generate() + { + $type = $this->type; + + if ($type != self::TYPE_AUTO) { + $type = $this->getValidatedType($type); + } + + $value = $this->value; + + if ($type == self::TYPE_AUTO) { + $type = $this->getAutoDeterminedType($value); + } + + if ($type == self::TYPE_ARRAY) { + foreach ($value as &$curValue) { + if ($curValue instanceof self) { + continue; + } + $curValue = new self($curValue, self::TYPE_AUTO, self::OUTPUT_MULTIPLE_LINE, $this->getConstants()); + } + } + + $output = ''; + + switch ($type) { + case self::TYPE_BOOLEAN: + case self::TYPE_BOOL: + $output .= ($value ? 'true' : 'false'); + break; + case self::TYPE_STRING: + $output .= self::escape($value); + break; + case self::TYPE_NULL: + $output .= 'null'; + break; + case self::TYPE_NUMBER: + case self::TYPE_INTEGER: + case self::TYPE_INT: + case self::TYPE_FLOAT: + case self::TYPE_DOUBLE: + case self::TYPE_CONSTANT: + $output .= $value; + break; + case self::TYPE_ARRAY: + $output .= 'array('; + if ($this->outputMode == self::OUTPUT_MULTIPLE_LINE) { + $output .= self::LINE_FEED . str_repeat($this->indentation, $this->arrayDepth + 1); + } + $outputParts = array(); + $noKeyIndex = 0; + foreach ($value as $n => $v) { + /* @var $v ValueGenerator */ + $v->setArrayDepth($this->arrayDepth + 1); + $partV = $v->generate(); + $short = false; + if (is_int($n)) { + if ($n === $noKeyIndex) { + $short = true; + $noKeyIndex++; + } else { + $noKeyIndex = max($n + 1, $noKeyIndex); + } + } + + if ($short) { + $outputParts[] = $partV; + } else { + $outputParts[] = (is_int($n) ? $n : self::escape($n)) . ' => ' . $partV; + } + } + $padding = ($this->outputMode == self::OUTPUT_MULTIPLE_LINE) + ? self::LINE_FEED . str_repeat($this->indentation, $this->arrayDepth + 1) + : ' '; + $output .= implode(',' . $padding, $outputParts); + if ($this->outputMode == self::OUTPUT_MULTIPLE_LINE) { + $output .= self::LINE_FEED . str_repeat($this->indentation, $this->arrayDepth); + } + $output .= ')'; + break; + case self::TYPE_OTHER: + default: + throw new Exception\RuntimeException( + sprintf('Type "%s" is unknown or cannot be used as property default value.', get_class($value)) + ); + } + + return $output; + } + + /** + * Quotes value for PHP code. + * + * @param string $input Raw string. + * @param bool $quote Whether add surrounding quotes or not. + * @return string PHP-ready code. + */ + public static function escape($input, $quote = true) + { + $output = addcslashes($input, "\\'"); + + // adds quoting strings + if ($quote) { + $output = "'" . $output . "'"; + } + + return $output; + } + + /** + * @param string $outputMode + * @return ValueGenerator + */ + public function setOutputMode($outputMode) + { + $this->outputMode = (string) $outputMode; + return $this; + } + + /** + * @return string + */ + public function getOutputMode() + { + return $this->outputMode; + } + + public function __toString() + { + return $this->generate(); + } +} diff --git a/library/Zend/Code/Generic/Prototype/PrototypeClassFactory.php b/library/Zend/Code/Generic/Prototype/PrototypeClassFactory.php new file mode 100755 index 0000000000..7c3a9bf1ba --- /dev/null +++ b/library/Zend/Code/Generic/Prototype/PrototypeClassFactory.php @@ -0,0 +1,121 @@ +addPrototype($prototype); + } + + if ($genericPrototype) { + $this->setGenericPrototype($genericPrototype); + } + } + + /** + * @param PrototypeInterface $prototype + * @throws Exception\InvalidArgumentException + */ + public function addPrototype(PrototypeInterface $prototype) + { + $prototypeName = $this->normalizeName($prototype->getName()); + + if (isset($this->prototypes[$prototypeName])) { + throw new Exception\InvalidArgumentException('A prototype with this name already exists in this manager'); + } + + $this->prototypes[$prototypeName] = $prototype; + } + + /** + * @param PrototypeGenericInterface $prototype + * @throws Exception\InvalidArgumentException + */ + public function setGenericPrototype(PrototypeGenericInterface $prototype) + { + if (isset($this->genericPrototype)) { + throw new Exception\InvalidArgumentException('A default prototype is already set'); + } + + $this->genericPrototype = $prototype; + } + + /** + * @param string $name + * @return string + */ + protected function normalizeName($name) + { + return str_replace(array('-', '_'), '', $name); + } + + /** + * @param string $name + * @return bool + */ + public function hasPrototype($name) + { + $name = $this->normalizeName($name); + return isset($this->prototypes[$name]); + } + + /** + * @param string $prototypeName + * @return PrototypeInterface + * @throws Exception\RuntimeException + */ + public function getClonedPrototype($prototypeName) + { + $prototypeName = $this->normalizeName($prototypeName); + + if (!$this->hasPrototype($prototypeName) && !isset($this->genericPrototype)) { + throw new Exception\RuntimeException('This tag name is not supported by this tag manager'); + } + + if (!$this->hasPrototype($prototypeName)) { + $newPrototype = clone $this->genericPrototype; + $newPrototype->setName($prototypeName); + } else { + $newPrototype = clone $this->prototypes[$prototypeName]; + } + + return $newPrototype; + } +} diff --git a/library/Zend/Code/Generic/Prototype/PrototypeGenericInterface.php b/library/Zend/Code/Generic/Prototype/PrototypeGenericInterface.php new file mode 100755 index 0000000000..3a5e44a883 --- /dev/null +++ b/library/Zend/Code/Generic/Prototype/PrototypeGenericInterface.php @@ -0,0 +1,18 @@ +setNamespace($namespace); + } + if ($uses) { + $this->setUses($uses); + } + } + + /** + * @param string $namespace + * @return NameInformation + */ + public function setNamespace($namespace) + { + $this->namespace = (string) $namespace; + return $this; + } + + /** + * @return string + */ + public function getNamespace() + { + return $this->namespace; + } + + /** + * @return bool + */ + public function hasNamespace() + { + return ($this->namespace != null); + } + + /** + * @param array $uses + * @return NameInformation + */ + public function setUses(array $uses) + { + $this->uses = array(); + $this->addUses($uses); + + return $this; + } + + /** + * @param array $uses + * @return NameInformation + */ + public function addUses(array $uses) + { + foreach ($uses as $use => $as) { + if (is_int($use)) { + $this->addUse($as); + } elseif (is_string($use)) { + $this->addUse($use, $as); + } + } + + return $this; + } + + /** + * @param array|string $use + * @param string $as + */ + public function addUse($use, $as = null) + { + if (is_array($use) && array_key_exists('use', $use) && array_key_exists('as', $use)) { + $uses = $use; + $use = $uses['use']; + $as = $uses['as']; + } + + $use = trim($use, '\\'); + if ($as === null) { + $as = trim($use, '\\'); + $nsSeparatorPosition = strrpos($as, '\\'); + if ($nsSeparatorPosition !== false && $nsSeparatorPosition !== 0 && $nsSeparatorPosition != strlen($as)) { + $as = substr($as, $nsSeparatorPosition + 1); + } + } + + $this->uses[$use] = $as; + } + + /** + * @return array + */ + public function getUses() + { + return $this->uses; + } + + /** + * @param string $name + * @return string + */ + public function resolveName($name) + { + if ($this->namespace && !$this->uses && strlen($name) > 0 && $name{0} != '\\') { + return $this->namespace . '\\' . $name; + } + + if (!$this->uses || strlen($name) <= 0 || $name{0} == '\\') { + return ltrim($name, '\\'); + } + + if ($this->namespace || $this->uses) { + $firstPart = $name; + if (($firstPartEnd = strpos($firstPart, '\\')) !== false) { + $firstPart = substr($firstPart, 0, $firstPartEnd); + } else { + $firstPartEnd = strlen($firstPart); + } + if (($fqns = array_search($firstPart, $this->uses)) !== false) { + return substr_replace($name, $fqns, 0, $firstPartEnd); + } + if ($this->namespace) { + return $this->namespace . '\\' . $name; + } + } + + return $name; + } +} diff --git a/library/Zend/Code/README.md b/library/Zend/Code/README.md new file mode 100755 index 0000000000..640084b572 --- /dev/null +++ b/library/Zend/Code/README.md @@ -0,0 +1,15 @@ +Code Component from ZF2 +======================= + +This is the Code component for ZF2. + +- File issues at https://github.com/zendframework/zf2/issues +- Create pull requests against https://github.com/zendframework/zf2 +- Documentation is at http://framework.zend.com/docs + +LICENSE +------- + +The files in this archive are released under the [Zend Framework +license](http://framework.zend.com/license), which is a 3-clause BSD license. + diff --git a/library/Zend/Code/Reflection/ClassReflection.php b/library/Zend/Code/Reflection/ClassReflection.php new file mode 100755 index 0000000000..86d1f14cbd --- /dev/null +++ b/library/Zend/Code/Reflection/ClassReflection.php @@ -0,0 +1,257 @@ +getFileName()); + + return $instance; + } + + /** + * Return the classes DocBlock reflection object + * + * @return DocBlockReflection + * @throws Exception\ExceptionInterface for missing DocBock or invalid reflection class + */ + public function getDocBlock() + { + if (isset($this->docBlock)) { + return $this->docBlock; + } + + if ('' == $this->getDocComment()) { + return false; + } + + $this->docBlock = new DocBlockReflection($this); + + return $this->docBlock; + } + + /** + * @param AnnotationManager $annotationManager + * @return AnnotationCollection + */ + public function getAnnotations(AnnotationManager $annotationManager) + { + $docComment = $this->getDocComment(); + + if ($docComment == '') { + return false; + } + + if ($this->annotations) { + return $this->annotations; + } + + $fileScanner = $this->createFileScanner($this->getFileName()); + $nameInformation = $fileScanner->getClassNameInformation($this->getName()); + + if (!$nameInformation) { + return false; + } + + $this->annotations = new AnnotationScanner($annotationManager, $docComment, $nameInformation); + + return $this->annotations; + } + + /** + * Return the start line of the class + * + * @param bool $includeDocComment + * @return int + */ + public function getStartLine($includeDocComment = false) + { + if ($includeDocComment && $this->getDocComment() != '') { + return $this->getDocBlock()->getStartLine(); + } + + return parent::getStartLine(); + } + + /** + * Return the contents of the class + * + * @param bool $includeDocBlock + * @return string + */ + public function getContents($includeDocBlock = true) + { + $fileName = $this->getFileName(); + + if (false === $fileName || ! file_exists($fileName)) { + return ''; + } + + $filelines = file($fileName); + $startnum = $this->getStartLine($includeDocBlock); + $endnum = $this->getEndLine() - $this->getStartLine(); + + // Ensure we get between the open and close braces + $lines = array_slice($filelines, $startnum, $endnum); + array_unshift($lines, $filelines[$startnum-1]); + + return strstr(implode('', $lines), '{'); + } + + /** + * Get all reflection objects of implemented interfaces + * + * @return ClassReflection[] + */ + public function getInterfaces() + { + $phpReflections = parent::getInterfaces(); + $zendReflections = array(); + while ($phpReflections && ($phpReflection = array_shift($phpReflections))) { + $instance = new ClassReflection($phpReflection->getName()); + $zendReflections[] = $instance; + unset($phpReflection); + } + unset($phpReflections); + + return $zendReflections; + } + + /** + * Return method reflection by name + * + * @param string $name + * @return MethodReflection + */ + public function getMethod($name) + { + $method = new MethodReflection($this->getName(), parent::getMethod($name)->getName()); + + return $method; + } + + /** + * Get reflection objects of all methods + * + * @param int $filter + * @return MethodReflection[] + */ + public function getMethods($filter = -1) + { + $methods = array(); + foreach (parent::getMethods($filter) as $method) { + $instance = new MethodReflection($this->getName(), $method->getName()); + $methods[] = $instance; + } + + return $methods; + } + + /** + * Get parent reflection class of reflected class + * + * @return ClassReflection|bool + */ + public function getParentClass() + { + $phpReflection = parent::getParentClass(); + if ($phpReflection) { + $zendReflection = new ClassReflection($phpReflection->getName()); + unset($phpReflection); + + return $zendReflection; + } + + return false; + } + + /** + * Return reflection property of this class by name + * + * @param string $name + * @return PropertyReflection + */ + public function getProperty($name) + { + $phpReflection = parent::getProperty($name); + $zendReflection = new PropertyReflection($this->getName(), $phpReflection->getName()); + unset($phpReflection); + + return $zendReflection; + } + + /** + * Return reflection properties of this class + * + * @param int $filter + * @return PropertyReflection[] + */ + public function getProperties($filter = -1) + { + $phpReflections = parent::getProperties($filter); + $zendReflections = array(); + while ($phpReflections && ($phpReflection = array_shift($phpReflections))) { + $instance = new PropertyReflection($this->getName(), $phpReflection->getName()); + $zendReflections[] = $instance; + unset($phpReflection); + } + unset($phpReflections); + + return $zendReflections; + } + + public function toString() + { + return parent::__toString(); + } + + public function __toString() + { + return parent::__toString(); + } + + /** + * Creates a new FileScanner instance. + * + * By having this as a seperate method it allows the method to be overridden + * if a different FileScanner is needed. + * + * @param string $filename + * + * @return FileScanner + */ + protected function createFileScanner($filename) + { + return new FileScanner($filename); + } +} diff --git a/library/Zend/Code/Reflection/DocBlock/Tag/AuthorTag.php b/library/Zend/Code/Reflection/DocBlock/Tag/AuthorTag.php new file mode 100755 index 0000000000..9afdee0878 --- /dev/null +++ b/library/Zend/Code/Reflection/DocBlock/Tag/AuthorTag.php @@ -0,0 +1,74 @@ +]*)\>)?(.*)$/u', $tagDocblockLine, $match)) { + return; + } + + if ($match[1] !== '') { + $this->authorName = rtrim($match[1]); + } + + if (isset($match[3]) && $match[3] !== '') { + $this->authorEmail = $match[3]; + } + } + + /** + * @return null|string + */ + public function getAuthorName() + { + return $this->authorName; + } + + /** + * @return null|string + */ + public function getAuthorEmail() + { + return $this->authorEmail; + } + + public function __toString() + { + return 'DocBlock Tag [ * @' . $this->getName() . ' ]' . PHP_EOL; + } +} diff --git a/library/Zend/Code/Reflection/DocBlock/Tag/GenericTag.php b/library/Zend/Code/Reflection/DocBlock/Tag/GenericTag.php new file mode 100755 index 0000000000..9f34810852 --- /dev/null +++ b/library/Zend/Code/Reflection/DocBlock/Tag/GenericTag.php @@ -0,0 +1,109 @@ +contentSplitCharacter = $contentSplitCharacter; + } + + /** + * @param string $tagDocBlockLine + * @return void + */ + public function initialize($tagDocBlockLine) + { + $this->parse($tagDocBlockLine); + } + + /** + * Get annotation tag name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * @return string + */ + public function getContent() + { + return $this->content; + } + + /** + * @param int $position + * @return string + */ + public function returnValue($position) + { + return $this->values[$position]; + } + + /** + * Serialize to string + * + * Required by Reflector + * + * @todo What should this do? + * @return string + */ + public function __toString() + { + return 'DocBlock Tag [ * @' . $this->name . ' ]' . PHP_EOL; + } + + /** + * @param string $docBlockLine + */ + protected function parse($docBlockLine) + { + $this->content = trim($docBlockLine); + $this->values = explode($this->contentSplitCharacter, $docBlockLine); + } +} diff --git a/library/Zend/Code/Reflection/DocBlock/Tag/LicenseTag.php b/library/Zend/Code/Reflection/DocBlock/Tag/LicenseTag.php new file mode 100755 index 0000000000..d1148ef621 --- /dev/null +++ b/library/Zend/Code/Reflection/DocBlock/Tag/LicenseTag.php @@ -0,0 +1,74 @@ +url = trim($match[1]); + } + + if (isset($match[2]) && $match[2] !== '') { + $this->licenseName = $match[2]; + } + } + + /** + * @return null|string + */ + public function getUrl() + { + return $this->url; + } + + /** + * @return null|string + */ + public function getLicenseName() + { + return $this->licenseName; + } + + public function __toString() + { + return 'DocBlock Tag [ * @' . $this->getName() . ' ]' . PHP_EOL; + } +} diff --git a/library/Zend/Code/Reflection/DocBlock/Tag/MethodTag.php b/library/Zend/Code/Reflection/DocBlock/Tag/MethodTag.php new file mode 100755 index 0000000000..50738bfea1 --- /dev/null +++ b/library/Zend/Code/Reflection/DocBlock/Tag/MethodTag.php @@ -0,0 +1,122 @@ +isStatic = true; + } + + if ($match[2] !== '') { + $this->types = explode('|', rtrim($match[2])); + } + + $this->methodName = $match[3]; + + if ($match[4] !== '') { + $this->description = $match[4]; + } + } + + /** + * Get return value type + * + * @return null|string + * @deprecated 2.0.4 use getTypes instead + */ + public function getReturnType() + { + if (empty($this->types)) { + return null; + } + + return $this->types[0]; + } + + public function getTypes() + { + return $this->types; + } + + /** + * @return string + */ + public function getMethodName() + { + return $this->methodName; + } + + /** + * @return null|string + */ + public function getDescription() + { + return $this->description; + } + + /** + * @return bool + */ + public function isStatic() + { + return $this->isStatic; + } + + public function __toString() + { + return 'DocBlock Tag [ * @' . $this->getName() . ' ]' . PHP_EOL; + } +} diff --git a/library/Zend/Code/Reflection/DocBlock/Tag/ParamTag.php b/library/Zend/Code/Reflection/DocBlock/Tag/ParamTag.php new file mode 100755 index 0000000000..b11a16e4ef --- /dev/null +++ b/library/Zend/Code/Reflection/DocBlock/Tag/ParamTag.php @@ -0,0 +1,98 @@ +types = explode('|', $matches[1]); + + if (isset($matches[2])) { + $this->variableName = $matches[2]; + } + + if (isset($matches[3])) { + $this->description = trim(preg_replace('#\s+#', ' ', $matches[3])); + } + } + + /** + * Get parameter variable type + * + * @return string + * @deprecated 2.0.4 use getTypes instead + */ + public function getType() + { + if (empty($this->types)) { + return ''; + } + + return $this->types[0]; + } + + public function getTypes() + { + return $this->types; + } + + /** + * Get parameter name + * + * @return string + */ + public function getVariableName() + { + return $this->variableName; + } + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } +} diff --git a/library/Zend/Code/Reflection/DocBlock/Tag/PhpDocTypedTagInterface.php b/library/Zend/Code/Reflection/DocBlock/Tag/PhpDocTypedTagInterface.php new file mode 100755 index 0000000000..01bea4b9d4 --- /dev/null +++ b/library/Zend/Code/Reflection/DocBlock/Tag/PhpDocTypedTagInterface.php @@ -0,0 +1,20 @@ +types = explode('|', rtrim($match[1])); + } + + if ($match[2] !== '') { + $this->propertyName = $match[2]; + } + + if ($match[3] !== '') { + $this->description = $match[3]; + } + } + + /** + * @return null|string + * @deprecated 2.0.4 use getTypes instead + */ + public function getType() + { + if (empty($this->types)) { + return null; + } + + return $this->types[0]; + } + + public function getTypes() + { + return $this->types; + } + + /** + * @return null|string + */ + public function getPropertyName() + { + return $this->propertyName; + } + + /** + * @return null|string + */ + public function getDescription() + { + return $this->description; + } + + public function __toString() + { + return 'DocBlock Tag [ * @' . $this->getName() . ' ]' . PHP_EOL; + } +} diff --git a/library/Zend/Code/Reflection/DocBlock/Tag/ReturnTag.php b/library/Zend/Code/Reflection/DocBlock/Tag/ReturnTag.php new file mode 100755 index 0000000000..f43d7e2e3f --- /dev/null +++ b/library/Zend/Code/Reflection/DocBlock/Tag/ReturnTag.php @@ -0,0 +1,75 @@ +types = explode('|', $matches[1]); + + if (isset($matches[2])) { + $this->description = trim(preg_replace('#\s+#', ' ', $matches[2])); + } + } + + /** + * @return string + * @deprecated 2.0.4 use getTypes instead + */ + public function getType() + { + if (empty($this->types)) { + return ''; + } + + return $this->types[0]; + } + + public function getTypes() + { + return $this->types; + } + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } +} diff --git a/library/Zend/Code/Reflection/DocBlock/Tag/TagInterface.php b/library/Zend/Code/Reflection/DocBlock/Tag/TagInterface.php new file mode 100755 index 0000000000..f34e154f64 --- /dev/null +++ b/library/Zend/Code/Reflection/DocBlock/Tag/TagInterface.php @@ -0,0 +1,21 @@ +types = explode('|', $matches[1]); + + if (isset($matches[2])) { + $this->description = $matches[2]; + } + } + + /** + * Get return variable type + * + * @return string + * @deprecated 2.0.4 use getTypes instead + */ + public function getType() + { + return implode('|', $this->getTypes()); + } + + /** + * @return array + */ + public function getTypes() + { + return $this->types; + } + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } +} diff --git a/library/Zend/Code/Reflection/DocBlock/TagManager.php b/library/Zend/Code/Reflection/DocBlock/TagManager.php new file mode 100755 index 0000000000..4d8f0da85c --- /dev/null +++ b/library/Zend/Code/Reflection/DocBlock/TagManager.php @@ -0,0 +1,48 @@ +addPrototype(new Tag\ParamTag()); + $this->addPrototype(new Tag\ReturnTag()); + $this->addPrototype(new Tag\MethodTag()); + $this->addPrototype(new Tag\PropertyTag()); + $this->addPrototype(new Tag\AuthorTag()); + $this->addPrototype(new Tag\LicenseTag()); + $this->addPrototype(new Tag\ThrowsTag()); + $this->setGenericPrototype(new Tag\GenericTag()); + } + + /** + * @param string $tagName + * @param string $content + * @return TagInterface + */ + public function createTag($tagName, $content = null) + { + /* @var TagInterface $newTag */ + $newTag = $this->getClonedPrototype($tagName); + + if ($content) { + $newTag->initialize($content); + } + + return $newTag; + } +} diff --git a/library/Zend/Code/Reflection/DocBlockReflection.php b/library/Zend/Code/Reflection/DocBlockReflection.php new file mode 100755 index 0000000000..a49021364b --- /dev/null +++ b/library/Zend/Code/Reflection/DocBlockReflection.php @@ -0,0 +1,293 @@ +initializeDefaultTags(); + } + $this->tagManager = $tagManager; + + if ($commentOrReflector instanceof Reflector) { + $this->reflector = $commentOrReflector; + if (!method_exists($commentOrReflector, 'getDocComment')) { + throw new Exception\InvalidArgumentException('Reflector must contain method "getDocComment"'); + } + /* @var MethodReflection $commentOrReflector */ + $this->docComment = $commentOrReflector->getDocComment(); + + // determine line numbers + $lineCount = substr_count($this->docComment, "\n"); + $this->startLine = $this->reflector->getStartLine() - $lineCount - 1; + $this->endLine = $this->reflector->getStartLine() - 1; + } elseif (is_string($commentOrReflector)) { + $this->docComment = $commentOrReflector; + } else { + throw new Exception\InvalidArgumentException(sprintf( + '%s must have a (string) DocComment or a Reflector in the constructor', + get_class($this) + )); + } + + if ($this->docComment == '') { + throw new Exception\InvalidArgumentException('DocComment cannot be empty'); + } + + $this->reflect(); + } + + /** + * Retrieve contents of DocBlock + * + * @return string + */ + public function getContents() + { + $this->reflect(); + + return $this->cleanDocComment; + } + + /** + * Get start line (position) of DocBlock + * + * @return int + */ + public function getStartLine() + { + $this->reflect(); + + return $this->startLine; + } + + /** + * Get last line (position) of DocBlock + * + * @return int + */ + public function getEndLine() + { + $this->reflect(); + + return $this->endLine; + } + + /** + * Get DocBlock short description + * + * @return string + */ + public function getShortDescription() + { + $this->reflect(); + + return $this->shortDescription; + } + + /** + * Get DocBlock long description + * + * @return string + */ + public function getLongDescription() + { + $this->reflect(); + + return $this->longDescription; + } + + /** + * Does the DocBlock contain the given annotation tag? + * + * @param string $name + * @return bool + */ + public function hasTag($name) + { + $this->reflect(); + foreach ($this->tags as $tag) { + if ($tag->getName() == $name) { + return true; + } + } + + return false; + } + + /** + * Retrieve the given DocBlock tag + * + * @param string $name + * @return DocBlockTagInterface|false + */ + public function getTag($name) + { + $this->reflect(); + foreach ($this->tags as $tag) { + if ($tag->getName() == $name) { + return $tag; + } + } + + return false; + } + + /** + * Get all DocBlock annotation tags + * + * @param string $filter + * @return DocBlockTagInterface[] + */ + public function getTags($filter = null) + { + $this->reflect(); + if ($filter === null || !is_string($filter)) { + return $this->tags; + } + + $returnTags = array(); + foreach ($this->tags as $tag) { + if ($tag->getName() == $filter) { + $returnTags[] = $tag; + } + } + + return $returnTags; + } + + /** + * Parse the DocBlock + * + * @return void + */ + protected function reflect() + { + if ($this->isReflected) { + return; + } + + $docComment = $this->docComment; // localize variable + + // create a clean docComment + $this->cleanDocComment = preg_replace("#[ \t]*(?:/\*\*|\*/|\*)[ ]{0,1}(.*)?#", '$1', $docComment); + $this->cleanDocComment = ltrim($this->cleanDocComment, "\r\n"); // @todo should be changed to remove first and last empty line + + $scanner = new DocBlockScanner($docComment); + $this->shortDescription = ltrim($scanner->getShortDescription()); + $this->longDescription = ltrim($scanner->getLongDescription()); + + foreach ($scanner->getTags() as $tag) { + $this->tags[] = $this->tagManager->createTag(ltrim($tag['name'], '@'), ltrim($tag['value'])); + } + + $this->isReflected = true; + } + + public function toString() + { + $str = "DocBlock [ /* DocBlock */ ] {" . PHP_EOL . PHP_EOL; + $str .= " - Tags [" . count($this->tags) . "] {" . PHP_EOL; + + foreach ($this->tags as $tag) { + $str .= " " . $tag; + } + + $str .= " }" . PHP_EOL; + $str .= "}" . PHP_EOL; + + return $str; + } + + /** + * Serialize to string + * + * Required by the Reflector interface + * + * @return string + */ + public function __toString() + { + return $this->toString(); + } +} diff --git a/library/Zend/Code/Reflection/Exception/BadMethodCallException.php b/library/Zend/Code/Reflection/Exception/BadMethodCallException.php new file mode 100755 index 0000000000..6eeb226fb5 --- /dev/null +++ b/library/Zend/Code/Reflection/Exception/BadMethodCallException.php @@ -0,0 +1,17 @@ +filePath = $fileRealPath; + $this->reflect(); + } + + /** + * Required by the Reflector interface. + * + * @todo What should this do? + * @return null + */ + public static function export() + { + return null; + } + + /** + * Return the file name of the reflected file + * + * @return string + */ + public function getFileName() + { + // @todo get file name from path + return $this->filePath; + } + + /** + * Get the start line - Always 1, staying consistent with the Reflection API + * + * @return int + */ + public function getStartLine() + { + return $this->startLine; + } + + /** + * Get the end line / number of lines + * + * @return int + */ + public function getEndLine() + { + return $this->endLine; + } + + /** + * @return string + */ + public function getDocComment() + { + return $this->docComment; + } + + /** + * @return DocBlockReflection + */ + public function getDocBlock() + { + if (!($docComment = $this->getDocComment())) { + return false; + } + + $instance = new DocBlockReflection($docComment); + + return $instance; + } + + /** + * @return array + */ + public function getNamespaces() + { + return $this->namespaces; + } + + /** + * @return string + */ + public function getNamespace() + { + if (count($this->namespaces) == 0) { + return null; + } + + return $this->namespaces[0]; + } + + /** + * @return array + */ + public function getUses() + { + return $this->uses; + } + + /** + * Return the reflection classes of the classes found inside this file + * + * @return ClassReflection[] + */ + public function getClasses() + { + $classes = array(); + foreach ($this->classes as $class) { + $classes[] = new ClassReflection($class); + } + + return $classes; + } + + /** + * Return the reflection functions of the functions found inside this file + * + * @return FunctionReflection[] + */ + public function getFunctions() + { + $functions = array(); + foreach ($this->functions as $function) { + $functions[] = new FunctionReflection($function); + } + + return $functions; + } + + /** + * Retrieve the reflection class of a given class found in this file + * + * @param null|string $name + * @return ClassReflection + * @throws Exception\InvalidArgumentException for invalid class name or invalid reflection class + */ + public function getClass($name = null) + { + if (null === $name) { + reset($this->classes); + $selected = current($this->classes); + + return new ClassReflection($selected); + } + + if (in_array($name, $this->classes)) { + return new ClassReflection($name); + } + + throw new Exception\InvalidArgumentException(sprintf( + 'Class by name %s not found.', + $name + )); + } + + /** + * Return the full contents of file + * + * @return string + */ + public function getContents() + { + return file_get_contents($this->filePath); + } + + public function toString() + { + return ''; // @todo + } + + /** + * Serialize to string + * + * Required by the Reflector interface + * + * @todo What should this serialization look like? + * @return string + */ + public function __toString() + { + return ''; + } + + /** + * This method does the work of "reflecting" the file + * + * Uses Zend\Code\Scanner\FileScanner to gather file information + * + * @return void + */ + protected function reflect() + { + $scanner = new CachingFileScanner($this->filePath); + $this->docComment = $scanner->getDocComment(); + $this->requiredFiles = $scanner->getIncludes(); + $this->classes = $scanner->getClassNames(); + $this->namespaces = $scanner->getNamespaces(); + $this->uses = $scanner->getUses(); + } + + /** + * Validate / check a file level DocBlock + * + * @param array $tokens Array of tokenizer tokens + * @return void + */ + protected function checkFileDocBlock($tokens) + { + foreach ($tokens as $token) { + $type = $token[0]; + $value = $token[1]; + $lineNum = $token[2]; + if (($type == T_OPEN_TAG) || ($type == T_WHITESPACE)) { + continue; + } elseif ($type == T_DOC_COMMENT) { + $this->docComment = $value; + $this->startLine = $lineNum + substr_count($value, "\n") + 1; + + return; + } else { + // Only whitespace is allowed before file DocBlocks + return; + } + } + } +} diff --git a/library/Zend/Code/Reflection/FunctionReflection.php b/library/Zend/Code/Reflection/FunctionReflection.php new file mode 100755 index 0000000000..c752f1ecac --- /dev/null +++ b/library/Zend/Code/Reflection/FunctionReflection.php @@ -0,0 +1,266 @@ +getDocComment())) { + throw new Exception\InvalidArgumentException(sprintf( + '%s does not have a DocBlock', + $this->getName() + )); + } + + $instance = new DocBlockReflection($comment); + + return $instance; + } + + /** + * Get start line (position) of function + * + * @param bool $includeDocComment + * @return int + */ + public function getStartLine($includeDocComment = false) + { + if ($includeDocComment) { + if ($this->getDocComment() != '') { + return $this->getDocBlock()->getStartLine(); + } + } + + return parent::getStartLine(); + } + + /** + * Get contents of function + * + * @param bool $includeDocBlock + * @return string + */ + public function getContents($includeDocBlock = true) + { + $fileName = $this->getFileName(); + if (false === $fileName) { + return ''; + } + + $startLine = $this->getStartLine(); + $endLine = $this->getEndLine(); + + // eval'd protect + if (preg_match('#\((\d+)\) : eval\(\)\'d code$#', $fileName, $matches)) { + $fileName = preg_replace('#\(\d+\) : eval\(\)\'d code$#', '', $fileName); + $startLine = $endLine = $matches[1]; + } + + $lines = array_slice( + file($fileName, FILE_IGNORE_NEW_LINES), + $startLine - 1, + ($endLine - ($startLine - 1)), + true + ); + + $functionLine = implode("\n", $lines); + + $content = ''; + if ($this->isClosure()) { + preg_match('#function\s*\([^\)]*\)\s*(use\s*\([^\)]+\))?\s*\{(.*\;)?\s*\}#s', $functionLine, $matches); + if (isset($matches[0])) { + $content = $matches[0]; + } + } else { + $name = substr($this->getName(), strrpos($this->getName(), '\\')+1); + preg_match('#function\s+' . preg_quote($name) . '\s*\([^\)]*\)\s*{([^{}]+({[^}]+})*[^}]+)?}#', $functionLine, $matches); + if (isset($matches[0])) { + $content = $matches[0]; + } + } + + $docComment = $this->getDocComment(); + + return $includeDocBlock && $docComment ? $docComment . "\n" . $content : $content; + } + + /** + * Get method prototype + * + * @return array + */ + public function getPrototype($format = FunctionReflection::PROTOTYPE_AS_ARRAY) + { + $returnType = 'mixed'; + $docBlock = $this->getDocBlock(); + if ($docBlock) { + $return = $docBlock->getTag('return'); + $returnTypes = $return->getTypes(); + $returnType = count($returnTypes) > 1 ? implode('|', $returnTypes) : $returnTypes[0]; + } + + $prototype = array( + 'namespace' => $this->getNamespaceName(), + 'name' => substr($this->getName(), strlen($this->getNamespaceName()) + 1), + 'return' => $returnType, + 'arguments' => array(), + ); + + $parameters = $this->getParameters(); + foreach ($parameters as $parameter) { + $prototype['arguments'][$parameter->getName()] = array( + 'type' => $parameter->getType(), + 'required' => !$parameter->isOptional(), + 'by_ref' => $parameter->isPassedByReference(), + 'default' => $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null, + ); + } + + if ($format == FunctionReflection::PROTOTYPE_AS_STRING) { + $line = $prototype['return'] . ' ' . $prototype['name'] . '('; + $args = array(); + foreach ($prototype['arguments'] as $name => $argument) { + $argsLine = ($argument['type'] ? $argument['type'] . ' ' : '') . ($argument['by_ref'] ? '&' : '') . '$' . $name; + if (!$argument['required']) { + $argsLine .= ' = ' . var_export($argument['default'], true); + } + $args[] = $argsLine; + } + $line .= implode(', ', $args); + $line .= ')'; + + return $line; + } + + return $prototype; + } + + /** + * Get function parameters + * + * @return ParameterReflection[] + */ + public function getParameters() + { + $phpReflections = parent::getParameters(); + $zendReflections = array(); + while ($phpReflections && ($phpReflection = array_shift($phpReflections))) { + $instance = new ParameterReflection($this->getName(), $phpReflection->getName()); + $zendReflections[] = $instance; + unset($phpReflection); + } + unset($phpReflections); + + return $zendReflections; + } + + /** + * Get return type tag + * + * @throws Exception\InvalidArgumentException + * @return DocBlockReflection + */ + public function getReturn() + { + $docBlock = $this->getDocBlock(); + if (!$docBlock->hasTag('return')) { + throw new Exception\InvalidArgumentException( + 'Function does not specify an @return annotation tag; cannot determine return type' + ); + } + + $tag = $docBlock->getTag('return'); + + return new DocBlockReflection('@return ' . $tag->getDescription()); + } + + /** + * Get method body + * + * @return string|bool + */ + public function getBody() + { + $fileName = $this->getFileName(); + if (false === $fileName) { + throw new Exception\InvalidArgumentException( + 'Cannot determine internals functions body' + ); + } + + $startLine = $this->getStartLine(); + $endLine = $this->getEndLine(); + + // eval'd protect + if (preg_match('#\((\d+)\) : eval\(\)\'d code$#', $fileName, $matches)) { + $fileName = preg_replace('#\(\d+\) : eval\(\)\'d code$#', '', $fileName); + $startLine = $endLine = $matches[1]; + } + + $lines = array_slice( + file($fileName, FILE_IGNORE_NEW_LINES), + $startLine - 1, + ($endLine - ($startLine - 1)), + true + ); + + $functionLine = implode("\n", $lines); + + $body = false; + if ($this->isClosure()) { + preg_match('#function\s*\([^\)]*\)\s*(use\s*\([^\)]+\))?\s*\{(.*\;)\s*\}#s', $functionLine, $matches); + if (isset($matches[2])) { + $body = $matches[2]; + } + } else { + $name = substr($this->getName(), strrpos($this->getName(), '\\')+1); + preg_match('#function\s+' . $name . '\s*\([^\)]*\)\s*{([^{}]+({[^}]+})*[^}]+)}#', $functionLine, $matches); + if (isset($matches[1])) { + $body = $matches[1]; + } + } + + return $body; + } + + public function toString() + { + return $this->__toString(); + } + + /** + * Required due to bug in php + * + * @return string + */ + public function __toString() + { + return parent::__toString(); + } +} diff --git a/library/Zend/Code/Reflection/MethodReflection.php b/library/Zend/Code/Reflection/MethodReflection.php new file mode 100755 index 0000000000..10a9b8ab4c --- /dev/null +++ b/library/Zend/Code/Reflection/MethodReflection.php @@ -0,0 +1,487 @@ +getDocComment()) { + return false; + } + + $instance = new DocBlockReflection($this); + + return $instance; + } + + /** + * @param AnnotationManager $annotationManager + * @return AnnotationScanner + */ + public function getAnnotations(AnnotationManager $annotationManager) + { + if (($docComment = $this->getDocComment()) == '') { + return false; + } + + if ($this->annotations) { + return $this->annotations; + } + + $cachingFileScanner = $this->createFileScanner($this->getFileName()); + $nameInformation = $cachingFileScanner->getClassNameInformation($this->getDeclaringClass()->getName()); + + if (!$nameInformation) { + return false; + } + + $this->annotations = new AnnotationScanner($annotationManager, $docComment, $nameInformation); + + return $this->annotations; + } + + /** + * Get start line (position) of method + * + * @param bool $includeDocComment + * @return int + */ + public function getStartLine($includeDocComment = false) + { + if ($includeDocComment) { + if ($this->getDocComment() != '') { + return $this->getDocBlock()->getStartLine(); + } + } + + return parent::getStartLine(); + } + + /** + * Get reflection of declaring class + * + * @return ClassReflection + */ + public function getDeclaringClass() + { + $phpReflection = parent::getDeclaringClass(); + $zendReflection = new ClassReflection($phpReflection->getName()); + unset($phpReflection); + + return $zendReflection; + } + + /** + * Get method prototype + * + * @return array + */ + public function getPrototype($format = MethodReflection::PROTOTYPE_AS_ARRAY) + { + $returnType = 'mixed'; + $docBlock = $this->getDocBlock(); + if ($docBlock) { + $return = $docBlock->getTag('return'); + $returnTypes = $return->getTypes(); + $returnType = count($returnTypes) > 1 ? implode('|', $returnTypes) : $returnTypes[0]; + } + + $declaringClass = $this->getDeclaringClass(); + $prototype = array( + 'namespace' => $declaringClass->getNamespaceName(), + 'class' => substr($declaringClass->getName(), strlen($declaringClass->getNamespaceName()) + 1), + 'name' => $this->getName(), + 'visibility' => ($this->isPublic() ? 'public' : ($this->isPrivate() ? 'private' : 'protected')), + 'return' => $returnType, + 'arguments' => array(), + ); + + $parameters = $this->getParameters(); + foreach ($parameters as $parameter) { + $prototype['arguments'][$parameter->getName()] = array( + 'type' => $parameter->getType(), + 'required' => !$parameter->isOptional(), + 'by_ref' => $parameter->isPassedByReference(), + 'default' => $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null, + ); + } + + if ($format == MethodReflection::PROTOTYPE_AS_STRING) { + $line = $prototype['visibility'] . ' ' . $prototype['return'] . ' ' . $prototype['name'] . '('; + $args = array(); + foreach ($prototype['arguments'] as $name => $argument) { + $argsLine = ($argument['type'] ? $argument['type'] . ' ' : '') . ($argument['by_ref'] ? '&' : '') . '$' . $name; + if (!$argument['required']) { + $argsLine .= ' = ' . var_export($argument['default'], true); + } + $args[] = $argsLine; + } + $line .= implode(', ', $args); + $line .= ')'; + + return $line; + } + + return $prototype; + } + + /** + * Get all method parameter reflection objects + * + * @return ParameterReflection[] + */ + public function getParameters() + { + $phpReflections = parent::getParameters(); + $zendReflections = array(); + while ($phpReflections && ($phpReflection = array_shift($phpReflections))) { + $instance = new ParameterReflection( + array($this->getDeclaringClass()->getName(), $this->getName()), + $phpReflection->getName() + ); + $zendReflections[] = $instance; + unset($phpReflection); + } + unset($phpReflections); + + return $zendReflections; + } + + /** + * Get method contents + * + * @param bool $includeDocBlock + * @return string|bool + */ + public function getContents($includeDocBlock = true) + { + $docComment = $this->getDocComment(); + $content = ($includeDocBlock && !empty($docComment)) ? $docComment . "\n" : ''; + $content .= $this->extractMethodContents(); + + return $content; + } + + /** + * Get method body + * + * @return string|bool + */ + public function getBody() + { + return $this->extractMethodContents(true); + } + + /** + * Tokenize method string and return concatenated body + * + * @param bool $bodyOnly + * @return string + */ + protected function extractMethodContents($bodyOnly=false) + { + $fileName = $this->getDeclaringClass()->getFileName(); + + if ((class_exists($this->class) && false === $fileName) || ! file_exists($fileName)) { + return ''; + } + + $lines = array_slice( + file($fileName, FILE_IGNORE_NEW_LINES), + $this->getStartLine() - 1, + ($this->getEndLine() - ($this->getStartLine() - 1)), + true + ); + + $functionLine = implode("\n", $lines); + $tokens = token_get_all(" $token) { + $tokenType = (is_array($token)) ? token_name($token[0]) : $token; + $tokenValue = (is_array($token)) ? $token[1] : $token; + + switch ($tokenType) { + case "T_FINAL": + case "T_ABSTRACT": + case "T_PUBLIC": + case "T_PROTECTED": + case "T_PRIVATE": + case "T_STATIC": + case "T_FUNCTION": + // check to see if we have a valid function + // then check if we are inside function and have a closure + if ($this->isValidFunction($tokens, $key, $this->getName())) { + if ($bodyOnly === false) { + //if first instance of tokenType grab prefixed whitespace + //and append to body + if ($capture === false) { + $body .= $this->extractPrefixedWhitespace($tokens, $key); + } + $body .= $tokenValue; + } + + $capture = true; + } else { + //closure test + if ($firstBrace && $tokenType == "T_FUNCTION") { + $body .= $tokenValue; + continue; + } + $capture = false; + continue; + } + break; + + case "{": + if ($capture === false) { + continue; + } + + if ($firstBrace === false) { + $firstBrace = true; + if ($bodyOnly === true) { + continue; + } + } + + $body .= $tokenValue; + break; + + case "}": + if ($capture === false) { + continue; + } + + //check to see if this is the last brace + if ($this->isEndingBrace($tokens, $key)) { + //capture the end brace if not bodyOnly + if ($bodyOnly === false) { + $body .= $tokenValue; + } + + break 2; + } + + $body .= $tokenValue; + break; + + default: + if ($capture === false) { + continue; + } + + // if returning body only wait for first brace before capturing + if ($bodyOnly === true && $firstBrace !== true) { + continue; + } + + $body .= $tokenValue; + break; + } + } + + //remove ending whitespace and return + return rtrim($body); + } + + /** + * Take current position and find any whitespace + * + * @param $haystack + * @param $position + * @return string + */ + protected function extractPrefixedWhitespace($haystack, $position) + { + $content = ''; + $count = count($haystack); + if ($position+1 == $count) { + return $content; + } + + for ($i = $position-1;$i >= 0;$i--) { + $tokenType = (is_array($haystack[$i])) ? token_name($haystack[$i][0]) : $haystack[$i]; + $tokenValue = (is_array($haystack[$i])) ? $haystack[$i][1] : $haystack[$i]; + + //search only for whitespace + if ($tokenType == "T_WHITESPACE") { + $content .= $tokenValue; + } else { + break; + } + } + + return $content; + } + + /** + * Test for ending brace + * + * @param $haystack + * @param $position + * @return bool + */ + protected function isEndingBrace($haystack, $position) + { + $count = count($haystack); + + //advance one position + $position = $position+1; + + if ($position == $count) { + return true; + } + + for ($i = $position;$i < $count; $i++) { + $tokenType = (is_array($haystack[$i])) ? token_name($haystack[$i][0]) : $haystack[$i]; + switch ($tokenType) { + case "T_FINAL": + case "T_ABSTRACT": + case "T_PUBLIC": + case "T_PROTECTED": + case "T_PRIVATE": + case "T_STATIC": + return true; + + case "T_FUNCTION": + // If a function is encountered and that function is not a closure + // then return true. otherwise the function is a closure, return false + if ($this->isValidFunction($haystack, $i)) { + return true; + } + return false; + + case "}": + case ";"; + case "T_BREAK": + case "T_CATCH": + case "T_DO": + case "T_ECHO": + case "T_ELSE": + case "T_ELSEIF": + case "T_EVAL": + case "T_EXIT": + case "T_FINALLY": + case "T_FOR": + case "T_FOREACH": + case "T_GOTO": + case "T_IF": + case "T_INCLUDE": + case "T_INCLUDE_ONCE": + case "T_PRINT": + case "T_STRING": + case "T_STRING_VARNAME": + case "T_THROW": + case "T_USE": + case "T_VARIABLE": + case "T_WHILE": + case "T_YIELD": + + return false; + } + } + } + + /** + * Test to see if current position is valid function or + * closure. Returns true if it's a function and NOT a closure + * + * @param $haystack + * @param $position + * @return bool + */ + protected function isValidFunction($haystack, $position, $functionName = null) + { + $isValid = false; + $count = count($haystack); + for ($i = $position+1;$i < $count; $i++) { + $tokenType = (is_array($haystack[$i])) ? token_name($haystack[$i][0]) : $haystack[$i]; + $tokenValue = (is_array($haystack[$i])) ? $haystack[$i][1] : $haystack[$i]; + + //check for occurance of ( or + if ($tokenType == "T_STRING") { + //check to see if function name is passed, if so validate against that + if ($functionName !== null && $tokenValue != $functionName) { + $isValid = false; + break; + } + + $isValid = true; + break; + } elseif ($tokenValue == "(") { + break; + } + } + + return $isValid; + } + + public function toString() + { + return parent::__toString(); + } + + public function __toString() + { + return parent::__toString(); + } + + /** + * Creates a new FileScanner instance. + * + * By having this as a seperate method it allows the method to be overridden + * if a different FileScanner is needed. + * + * @param string $filename + * + * @return CachingFileScanner + */ + protected function createFileScanner($filename) + { + return new CachingFileScanner($filename); + } +} diff --git a/library/Zend/Code/Reflection/ParameterReflection.php b/library/Zend/Code/Reflection/ParameterReflection.php new file mode 100755 index 0000000000..e6239abce7 --- /dev/null +++ b/library/Zend/Code/Reflection/ParameterReflection.php @@ -0,0 +1,110 @@ +getName()); + unset($phpReflection); + + return $zendReflection; + } + + /** + * Get class reflection object + * + * @return ClassReflection + */ + public function getClass() + { + $phpReflection = parent::getClass(); + if ($phpReflection == null) { + return null; + } + + $zendReflection = new ClassReflection($phpReflection->getName()); + unset($phpReflection); + + return $zendReflection; + } + + /** + * Get declaring function reflection object + * + * @return FunctionReflection|MethodReflection + */ + public function getDeclaringFunction() + { + $phpReflection = parent::getDeclaringFunction(); + if ($phpReflection instanceof \ReflectionMethod) { + $zendReflection = new MethodReflection($this->getDeclaringClass()->getName(), $phpReflection->getName()); + } else { + $zendReflection = new FunctionReflection($phpReflection->getName()); + } + unset($phpReflection); + + return $zendReflection; + } + + /** + * Get parameter type + * + * @return string + */ + public function getType() + { + if ($this->isArray()) { + return 'array'; + } elseif (method_exists($this, 'isCallable') && $this->isCallable()) { + return 'callable'; + } + + if (($class = $this->getClass()) instanceof \ReflectionClass) { + return $class->getName(); + } + + $docBlock = $this->getDeclaringFunction()->getDocBlock(); + if (!$docBlock instanceof DocBlockReflection) { + return null; + } + + $params = $docBlock->getTags('param'); + if (isset($params[$this->getPosition()])) { + return $params[$this->getPosition()]->getType(); + } + + return null; + } + + public function toString() + { + return parent::__toString(); + } + + public function __toString() + { + return parent::__toString(); + } +} diff --git a/library/Zend/Code/Reflection/PropertyReflection.php b/library/Zend/Code/Reflection/PropertyReflection.php new file mode 100755 index 0000000000..c9b5a8d255 --- /dev/null +++ b/library/Zend/Code/Reflection/PropertyReflection.php @@ -0,0 +1,111 @@ +getName()); + unset($phpReflection); + + return $zendReflection; + } + + /** + * Get DocBlock comment + * + * @return string|false False if no DocBlock defined + */ + public function getDocComment() + { + return parent::getDocComment(); + } + + /** + * @return false|DocBlockReflection + */ + public function getDocBlock() + { + if (!($docComment = $this->getDocComment())) { + return false; + } + + $docBlockReflection = new DocBlockReflection($docComment); + + return $docBlockReflection; + } + + /** + * @param AnnotationManager $annotationManager + * @return AnnotationScanner + */ + public function getAnnotations(AnnotationManager $annotationManager) + { + if (null !== $this->annotations) { + return $this->annotations; + } + + if (($docComment = $this->getDocComment()) == '') { + return false; + } + + $class = $this->getDeclaringClass(); + $cachingFileScanner = $this->createFileScanner($class->getFileName()); + $nameInformation = $cachingFileScanner->getClassNameInformation($class->getName()); + + if (!$nameInformation) { + return false; + } + + $this->annotations = new AnnotationScanner($annotationManager, $docComment, $nameInformation); + + return $this->annotations; + } + + public function toString() + { + return $this->__toString(); + } + + /** + * Creates a new FileScanner instance. + * + * By having this as a seperate method it allows the method to be overridden + * if a different FileScanner is needed. + * + * @param string $filename + * + * @return CachingFileScanner + */ + protected function createFileScanner($filename) + { + return new CachingFileScanner($filename); + } +} diff --git a/library/Zend/Code/Reflection/ReflectionInterface.php b/library/Zend/Code/Reflection/ReflectionInterface.php new file mode 100755 index 0000000000..ed5ffd46c0 --- /dev/null +++ b/library/Zend/Code/Reflection/ReflectionInterface.php @@ -0,0 +1,20 @@ +directories as $scanner) { + $classes += $scanner->getClasses(); + } + if ($returnScannerClass) { + foreach ($classes as $index => $class) { + $classes[$index] = $this->getClass($class, $returnScannerClass, $returnDerivedScannerClass); + } + } + + return $classes; + } + + /** + * @param string $class + * @return bool + */ + public function hasClass($class) + { + foreach ($this->directories as $scanner) { + if ($scanner->hasClass($class)) { + break; + } else { + unset($scanner); + } + } + + return (isset($scanner)); + } + + /** + * @param string $class + * @param bool $returnScannerClass + * @param bool $returnDerivedScannerClass + * @return ClassScanner|DerivedClassScanner + * @throws Exception\RuntimeException + */ + public function getClass($class, $returnScannerClass = true, $returnDerivedScannerClass = false) + { + foreach ($this->directories as $scanner) { + if ($scanner->hasClass($class)) { + break; + } else { + unset($scanner); + } + } + + if (!isset($scanner)) { + throw new Exception\RuntimeException('Class by that name was not found.'); + } + + $classScanner = $scanner->getClass($class); + + return new DerivedClassScanner($classScanner, $this); + } + + /** + * @param bool $returnScannerClass + */ + public function getFunctions($returnScannerClass = false) + { + $this->scan(); + + if (!$returnScannerClass) { + $functions = array(); + foreach ($this->infos as $info) { + if ($info['type'] == 'function') { + $functions[] = $info['name']; + } + } + + return $functions; + } + $scannerClass = new FunctionScanner(); + // @todo + } +} diff --git a/library/Zend/Code/Scanner/AnnotationScanner.php b/library/Zend/Code/Scanner/AnnotationScanner.php new file mode 100755 index 0000000000..76e8144c01 --- /dev/null +++ b/library/Zend/Code/Scanner/AnnotationScanner.php @@ -0,0 +1,338 @@ +annotationManager = $annotationManager; + $this->docComment = $docComment; + $this->nameInformation = $nameInformation; + $this->scan($this->tokenize()); + } + + /** + * @param NameInformation $nameInformation + */ + public function setNameInformation(NameInformation $nameInformation) + { + $this->nameInformation = $nameInformation; + } + + /** + * @param array $tokens + */ + protected function scan(array $tokens) + { + $annotations = array(); + $annotationIndex = -1; + $contentEnd = false; + + reset($tokens); + + SCANNER_TOP: + $token = current($tokens); + + switch ($token[0]) { + + case 'ANNOTATION_CLASS': + + $contentEnd = false; + $annotationIndex++; + $class = substr($token[1], 1); + $class = $this->nameInformation->resolveName($class); + $annotations[$annotationIndex] = array($class, null); + goto SCANNER_CONTINUE; + // goto no break needed + + case 'ANNOTATION_CONTENT_START': + + $annotations[$annotationIndex][1] = ''; + //fall-through + + case 'ANNOTATION_CONTENT_END': + case 'ANNOTATION_CONTENT': + case 'ANNOTATION_WHITESPACE': + case 'ANNOTATION_NEWLINE': + + if (!$contentEnd && isset($annotations[$annotationIndex]) && is_string($annotations[$annotationIndex][1])) { + $annotations[$annotationIndex][1] .= $token[1]; + } + + if ($token[0] === 'ANNOTATION_CONTENT_END') { + $contentEnd = true; + } + + goto SCANNER_CONTINUE; + } + + SCANNER_CONTINUE: + if (next($tokens) === false) { + goto SCANNER_END; + } + goto SCANNER_TOP; + + SCANNER_END: + + foreach ($annotations as $annotation) { + $annotation[] = '@' . $annotation[0] . $annotation[1]; + $annotationObject = $this->annotationManager->createAnnotation($annotation); + if ($annotationObject) { + $this->append($annotationObject); + } + } + } + + /** + * @return array + */ + protected function tokenize() + { + static $CONTEXT_DOCBLOCK = 0x01; + static $CONTEXT_ASTERISK = 0x02; + static $CONTEXT_CLASS = 0x04; + static $CONTEXT_CONTENT = 0x08; + + $context = 0x00; + $stream = $this->docComment; + $streamIndex = null; + $tokens = array(); + $tokenIndex = null; + $currentChar = null; + $currentWord = null; + $currentLine = null; + + $annotationParentCount = 0; + + $MACRO_STREAM_ADVANCE_CHAR = function ($positionsForward = 1) use (&$stream, &$streamIndex, &$currentChar, &$currentWord, &$currentLine) { + $positionsForward = ($positionsForward > 0) ? $positionsForward : 1; + $streamIndex = ($streamIndex === null) ? 0 : $streamIndex + $positionsForward; + if (!isset($stream[$streamIndex])) { + $currentChar = false; + + return false; + } + $currentChar = $stream[$streamIndex]; + $matches = array(); + $currentLine = (preg_match('#(.*)\n#', $stream, $matches, null, $streamIndex) === 1) ? $matches[1] : substr($stream, $streamIndex); + if ($currentChar === ' ') { + $currentWord = (preg_match('#( +)#', $currentLine, $matches) === 1) ? $matches[1] : $currentLine; + } else { + $currentWord = (($matches = strpos($currentLine, ' ')) !== false) ? substr($currentLine, 0, $matches) : $currentLine; + } + + return $currentChar; + }; + $MACRO_STREAM_ADVANCE_WORD = function () use (&$currentWord, &$MACRO_STREAM_ADVANCE_CHAR) { + return $MACRO_STREAM_ADVANCE_CHAR(strlen($currentWord)); + }; + $MACRO_STREAM_ADVANCE_LINE = function () use (&$currentLine, &$MACRO_STREAM_ADVANCE_CHAR) { + return $MACRO_STREAM_ADVANCE_CHAR(strlen($currentLine)); + }; + $MACRO_TOKEN_ADVANCE = function () use (&$tokenIndex, &$tokens) { + $tokenIndex = ($tokenIndex === null) ? 0 : $tokenIndex + 1; + $tokens[$tokenIndex] = array('ANNOTATION_UNKNOWN', ''); + }; + $MACRO_TOKEN_SET_TYPE = function ($type) use (&$tokenIndex, &$tokens) { + $tokens[$tokenIndex][0] = $type; + }; + $MACRO_TOKEN_APPEND_CHAR = function () use (&$currentChar, &$tokens, &$tokenIndex) { + $tokens[$tokenIndex][1] .= $currentChar; + }; + $MACRO_TOKEN_APPEND_WORD = function () use (&$currentWord, &$tokens, &$tokenIndex) { + $tokens[$tokenIndex][1] .= $currentWord; + }; + $MACRO_TOKEN_APPEND_LINE = function () use (&$currentLine, &$tokens, &$tokenIndex) { + $tokens[$tokenIndex][1] .= $currentLine; + }; + $MACRO_HAS_CONTEXT = function ($which) use (&$context) { + return (($context & $which) === $which); + }; + + $MACRO_STREAM_ADVANCE_CHAR(); + $MACRO_TOKEN_ADVANCE(); + + TOKENIZER_TOP: + + if ($context === 0x00 && $currentChar === '/' && $currentWord === '/**') { + $MACRO_TOKEN_SET_TYPE('ANNOTATION_COMMENTSTART'); + $MACRO_TOKEN_APPEND_WORD(); + $MACRO_TOKEN_ADVANCE(); + $context |= $CONTEXT_DOCBLOCK; + $context |= $CONTEXT_ASTERISK; + if ($MACRO_STREAM_ADVANCE_WORD() === false) { + goto TOKENIZER_END; + } + goto TOKENIZER_TOP; + } + + if ($MACRO_HAS_CONTEXT($CONTEXT_CLASS)) { + if (in_array($currentChar, array(' ', '(', "\n"))) { + $context &= ~$CONTEXT_CLASS; + $MACRO_TOKEN_ADVANCE(); + } else { + $MACRO_TOKEN_APPEND_CHAR(); + if ($MACRO_STREAM_ADVANCE_CHAR() === false) { + goto TOKENIZER_END; + } + goto TOKENIZER_TOP; + } + } + + if ($currentChar === "\n") { + $MACRO_TOKEN_SET_TYPE('ANNOTATION_NEWLINE'); + $MACRO_TOKEN_APPEND_CHAR(); + $MACRO_TOKEN_ADVANCE(); + $context &= ~$CONTEXT_ASTERISK; + $context &= ~$CONTEXT_CLASS; + if ($MACRO_STREAM_ADVANCE_CHAR() === false) { + goto TOKENIZER_END; + } + goto TOKENIZER_TOP; + } + + if ($currentChar === ' ') { + $MACRO_TOKEN_SET_TYPE(($MACRO_HAS_CONTEXT($CONTEXT_ASTERISK)) ? 'ANNOTATION_WHITESPACE' : 'ANNOTATION_WHITESPACE_INDENT'); + $MACRO_TOKEN_APPEND_WORD(); + $MACRO_TOKEN_ADVANCE(); + if ($MACRO_STREAM_ADVANCE_WORD() === false) { + goto TOKENIZER_END; + } + goto TOKENIZER_TOP; + } + + if ($MACRO_HAS_CONTEXT($CONTEXT_CONTENT) && $MACRO_HAS_CONTEXT($CONTEXT_ASTERISK)) { + $MACRO_TOKEN_SET_TYPE('ANNOTATION_CONTENT'); + $annotationParentCount += substr_count($currentWord, '('); + $annotationParentCount -= substr_count($currentWord, ')'); + + if ($annotationParentCount === 0) { + $context &= ~$CONTEXT_CONTENT; + $MACRO_TOKEN_SET_TYPE('ANNOTATION_CONTENT_END'); + } + $MACRO_TOKEN_APPEND_WORD(); + $MACRO_TOKEN_ADVANCE(); + if ($MACRO_STREAM_ADVANCE_WORD() === false) { + goto TOKENIZER_END; + } + goto TOKENIZER_TOP; + } + + if ($currentChar === '(' && $tokens[$tokenIndex - 1][0] === 'ANNOTATION_CLASS') { + $context |= $CONTEXT_CONTENT; + $annotationParentCount = 1; + $MACRO_TOKEN_SET_TYPE('ANNOTATION_CONTENT_START'); + $MACRO_TOKEN_APPEND_CHAR(); + $MACRO_TOKEN_ADVANCE(); + if ($MACRO_STREAM_ADVANCE_CHAR() === false) { + goto TOKENIZER_END; + } + goto TOKENIZER_TOP; + } + + if ($MACRO_HAS_CONTEXT($CONTEXT_DOCBLOCK) && $currentWord === '*/') { + $MACRO_TOKEN_SET_TYPE('ANNOTATION_COMMENTEND'); + $MACRO_TOKEN_APPEND_WORD(); + $MACRO_TOKEN_ADVANCE(); + $context &= ~$CONTEXT_DOCBLOCK; + if ($MACRO_STREAM_ADVANCE_WORD() === false) { + goto TOKENIZER_END; + } + goto TOKENIZER_TOP; + } + + if ($currentChar === '*') { + if ($MACRO_HAS_CONTEXT($CONTEXT_DOCBLOCK) && ($MACRO_HAS_CONTEXT($CONTEXT_ASTERISK))) { + $MACRO_TOKEN_SET_TYPE('ANNOTATION_IGNORE'); + } else { + $MACRO_TOKEN_SET_TYPE('ANNOTATION_ASTERISK'); + $context |= $CONTEXT_ASTERISK; + } + $MACRO_TOKEN_APPEND_CHAR(); + $MACRO_TOKEN_ADVANCE(); + if ($MACRO_STREAM_ADVANCE_CHAR() === false) { + goto TOKENIZER_END; + } + goto TOKENIZER_TOP; + } + + if ($currentChar === '@') { + $MACRO_TOKEN_SET_TYPE('ANNOTATION_CLASS'); + $context |= $CONTEXT_CLASS; + $MACRO_TOKEN_APPEND_CHAR(); + if ($MACRO_STREAM_ADVANCE_CHAR() === false) { + goto TOKENIZER_END; + } + goto TOKENIZER_TOP; + } + + TOKENIZER_CONTINUE: + + if ($context && $CONTEXT_CONTENT) { + $MACRO_TOKEN_APPEND_CHAR(); + if ($MACRO_STREAM_ADVANCE_CHAR() === false) { + goto TOKENIZER_END; + } + } else { + $MACRO_TOKEN_SET_TYPE('ANNOTATION_IGNORE'); + $MACRO_TOKEN_APPEND_LINE(); + $MACRO_TOKEN_ADVANCE(); + if ($MACRO_STREAM_ADVANCE_LINE() === false) { + goto TOKENIZER_END; + } + } + goto TOKENIZER_TOP; + + TOKENIZER_END: + + array_pop($tokens); + + return $tokens; + } +} diff --git a/library/Zend/Code/Scanner/CachingFileScanner.php b/library/Zend/Code/Scanner/CachingFileScanner.php new file mode 100755 index 0000000000..cb72867046 --- /dev/null +++ b/library/Zend/Code/Scanner/CachingFileScanner.php @@ -0,0 +1,160 @@ +fileScanner = static::$cache[$cacheId]; + } else { + $this->fileScanner = new FileScanner($file, $annotationManager); + static::$cache[$cacheId] = $this->fileScanner; + } + } + + /** + * @return void + */ + public static function clearCache() + { + static::$cache = array(); + } + + /** + * @return AnnotationManager + */ + public function getAnnotationManager() + { + return $this->fileScanner->getAnnotationManager(); + } + + /** + * @return array|null|string + */ + public function getFile() + { + return $this->fileScanner->getFile(); + } + + /** + * @return null|string + */ + public function getDocComment() + { + return $this->fileScanner->getDocComment(); + } + + /** + * @return array + */ + public function getNamespaces() + { + return $this->fileScanner->getNamespaces(); + } + + /** + * @param null|string $namespace + * @return array|null + */ + public function getUses($namespace = null) + { + return $this->fileScanner->getUses($namespace); + } + + /** + * @return array + */ + public function getIncludes() + { + return $this->fileScanner->getIncludes(); + } + + /** + * @return array + */ + public function getClassNames() + { + return $this->fileScanner->getClassNames(); + } + + /** + * @return array + */ + public function getClasses() + { + return $this->fileScanner->getClasses(); + } + + /** + * @param int|string $className + * @return ClassScanner + */ + public function getClass($className) + { + return $this->fileScanner->getClass($className); + } + + /** + * @param string $className + * @return bool|null|NameInformation + */ + public function getClassNameInformation($className) + { + return $this->fileScanner->getClassNameInformation($className); + } + + /** + * @return array + */ + public function getFunctionNames() + { + return $this->fileScanner->getFunctionNames(); + } + + /** + * @return array + */ + public function getFunctions() + { + return $this->fileScanner->getFunctions(); + } +} diff --git a/library/Zend/Code/Scanner/ClassScanner.php b/library/Zend/Code/Scanner/ClassScanner.php new file mode 100755 index 0000000000..2efc5539c1 --- /dev/null +++ b/library/Zend/Code/Scanner/ClassScanner.php @@ -0,0 +1,972 @@ +tokens = $classTokens; + $this->nameInformation = $nameInformation; + } + + /** + * Get annotations + * + * @param Annotation\AnnotationManager $annotationManager + * @return Annotation\AnnotationCollection + */ + public function getAnnotations(Annotation\AnnotationManager $annotationManager) + { + if (($docComment = $this->getDocComment()) == '') { + return false; + } + + return new AnnotationScanner($annotationManager, $docComment, $this->nameInformation); + } + + /** + * Return documentation comment + * + * @return null|string + */ + public function getDocComment() + { + $this->scan(); + + return $this->docComment; + } + + /** + * Return documentation block + * + * @return false|DocBlockScanner + */ + public function getDocBlock() + { + if (!$docComment = $this->getDocComment()) { + return false; + } + + return new DocBlockScanner($docComment); + } + + /** + * Return a name of class + * + * @return null|string + */ + public function getName() + { + $this->scan(); + return $this->name; + } + + /** + * Return short name of class + * + * @return null|string + */ + public function getShortName() + { + $this->scan(); + return $this->shortName; + } + + /** + * Return number of first line + * + * @return int|null + */ + public function getLineStart() + { + $this->scan(); + return $this->lineStart; + } + + /** + * Return number of last line + * + * @return int|null + */ + public function getLineEnd() + { + $this->scan(); + return $this->lineEnd; + } + + /** + * Verify if class is final + * + * @return bool + */ + public function isFinal() + { + $this->scan(); + return $this->isFinal; + } + + /** + * Verify if class is instantiable + * + * @return bool + */ + public function isInstantiable() + { + $this->scan(); + return (!$this->isAbstract && !$this->isInterface); + } + + /** + * Verify if class is an abstract class + * + * @return bool + */ + public function isAbstract() + { + $this->scan(); + return $this->isAbstract; + } + + /** + * Verify if class is an interface + * + * @return bool + */ + public function isInterface() + { + $this->scan(); + return $this->isInterface; + } + + /** + * Verify if class has parent + * + * @return bool + */ + public function hasParentClass() + { + $this->scan(); + return ($this->parentClass != null); + } + + /** + * Return a name of parent class + * + * @return null|string + */ + public function getParentClass() + { + $this->scan(); + return $this->parentClass; + } + + /** + * Return a list of interface names + * + * @return array + */ + public function getInterfaces() + { + $this->scan(); + return $this->interfaces; + } + + /** + * Return a list of constant names + * + * @return array + */ + public function getConstantNames() + { + $this->scan(); + + $return = array(); + foreach ($this->infos as $info) { + if ($info['type'] != 'constant') { + continue; + } + + $return[] = $info['name']; + } + + return $return; + } + + /** + * Return a list of constants + * + * @param bool $namesOnly Set false to return instances of ConstantScanner + * @return array|ConstantScanner[] + */ + public function getConstants($namesOnly = true) + { + if (true === $namesOnly) { + trigger_error('Use method getConstantNames() instead', E_USER_DEPRECATED); + return $this->getConstantNames(); + } + + $this->scan(); + + $return = array(); + foreach ($this->infos as $info) { + if ($info['type'] != 'constant') { + continue; + } + + $return[] = $this->getConstant($info['name']); + } + + return $return; + } + + /** + * Return a single constant by given name or index of info + * + * @param string|int $constantNameOrInfoIndex + * @throws Exception\InvalidArgumentException + * @return bool|ConstantScanner + */ + public function getConstant($constantNameOrInfoIndex) + { + $this->scan(); + + if (is_int($constantNameOrInfoIndex)) { + $info = $this->infos[$constantNameOrInfoIndex]; + if ($info['type'] != 'constant') { + throw new Exception\InvalidArgumentException('Index of info offset is not about a constant'); + } + } elseif (is_string($constantNameOrInfoIndex)) { + $constantFound = false; + foreach ($this->infos as $info) { + if ($info['type'] === 'constant' && $info['name'] === $constantNameOrInfoIndex) { + $constantFound = true; + break; + } + } + if (!$constantFound) { + return false; + } + } else { + throw new Exception\InvalidArgumentException('Invalid constant name of info index type. Must be of type int or string'); + } + if (!isset($info)) { + return false; + } + $p = new ConstantScanner( + array_slice($this->tokens, $info['tokenStart'], $info['tokenEnd'] - $info['tokenStart'] + 1), + $this->nameInformation + ); + $p->setClass($this->name); + $p->setScannerClass($this); + return $p; + } + + /** + * Verify if class has constant + * + * @param string $name + * @return bool + */ + public function hasConstant($name) + { + $this->scan(); + + foreach ($this->infos as $info) { + if ($info['type'] === 'constant' && $info['name'] === $name) { + return true; + } + } + + return false; + } + + /** + * Return a list of property names + * + * @return array + */ + public function getPropertyNames() + { + $this->scan(); + + $return = array(); + foreach ($this->infos as $info) { + if ($info['type'] != 'property') { + continue; + } + + $return[] = $info['name']; + } + + return $return; + } + + /** + * Return a list of properties + * + * @return PropertyScanner + */ + public function getProperties() + { + $this->scan(); + + $return = array(); + foreach ($this->infos as $info) { + if ($info['type'] != 'property') { + continue; + } + + $return[] = $this->getProperty($info['name']); + } + + return $return; + } + + /** + * Return a single property by given name or index of info + * + * @param string|int $propertyNameOrInfoIndex + * @throws Exception\InvalidArgumentException + * @return bool|PropertyScanner + */ + public function getProperty($propertyNameOrInfoIndex) + { + $this->scan(); + + if (is_int($propertyNameOrInfoIndex)) { + $info = $this->infos[$propertyNameOrInfoIndex]; + if ($info['type'] != 'property') { + throw new Exception\InvalidArgumentException('Index of info offset is not about a property'); + } + } elseif (is_string($propertyNameOrInfoIndex)) { + $propertyFound = false; + foreach ($this->infos as $info) { + if ($info['type'] === 'property' && $info['name'] === $propertyNameOrInfoIndex) { + $propertyFound = true; + break; + } + } + if (!$propertyFound) { + return false; + } + } else { + throw new Exception\InvalidArgumentException('Invalid property name of info index type. Must be of type int or string'); + } + if (!isset($info)) { + return false; + } + $p = new PropertyScanner( + array_slice($this->tokens, $info['tokenStart'], $info['tokenEnd'] - $info['tokenStart'] + 1), + $this->nameInformation + ); + $p->setClass($this->name); + $p->setScannerClass($this); + return $p; + } + + /** + * Verify if class has property + * + * @param string $name + * @return bool + */ + public function hasProperty($name) + { + $this->scan(); + + foreach ($this->infos as $info) { + if ($info['type'] === 'property' && $info['name'] === $name) { + return true; + } + } + + return false; + } + + /** + * Return a list of method names + * + * @return array + */ + public function getMethodNames() + { + $this->scan(); + + $return = array(); + foreach ($this->infos as $info) { + if ($info['type'] != 'method') { + continue; + } + + $return[] = $info['name']; + } + + return $return; + } + + /** + * Return a list of methods + * + * @return MethodScanner[] + */ + public function getMethods() + { + $this->scan(); + + $return = array(); + foreach ($this->infos as $info) { + if ($info['type'] != 'method') { + continue; + } + + $return[] = $this->getMethod($info['name']); + } + + return $return; + } + + /** + * Return a single method by given name or index of info + * + * @param string|int $methodNameOrInfoIndex + * @throws Exception\InvalidArgumentException + * @return MethodScanner + */ + public function getMethod($methodNameOrInfoIndex) + { + $this->scan(); + + if (is_int($methodNameOrInfoIndex)) { + $info = $this->infos[$methodNameOrInfoIndex]; + if ($info['type'] != 'method') { + throw new Exception\InvalidArgumentException('Index of info offset is not about a method'); + } + } elseif (is_string($methodNameOrInfoIndex)) { + $methodFound = false; + foreach ($this->infos as $info) { + if ($info['type'] === 'method' && $info['name'] === $methodNameOrInfoIndex) { + $methodFound = true; + break; + } + } + if (!$methodFound) { + return false; + } + } + if (!isset($info)) { + // @todo find a way to test this + die('Massive Failure, test this'); + } + + $m = new MethodScanner( + array_slice($this->tokens, $info['tokenStart'], $info['tokenEnd'] - $info['tokenStart'] + 1), + $this->nameInformation + ); + $m->setClass($this->name); + $m->setScannerClass($this); + + return $m; + } + + /** + * Verify if class has method by given name + * + * @param string $name + * @return bool + */ + public function hasMethod($name) + { + $this->scan(); + + foreach ($this->infos as $info) { + if ($info['type'] === 'method' && $info['name'] === $name) { + return true; + } + } + + return false; + } + + public static function export() + { + // @todo + } + + public function __toString() + { + // @todo + } + + /** + * Scan tokens + * + * @return void + * @throws Exception\RuntimeException + */ + protected function scan() + { + if ($this->isScanned) { + return; + } + + if (!$this->tokens) { + throw new Exception\RuntimeException('No tokens were provided'); + } + + /** + * Variables & Setup + */ + + $tokens = &$this->tokens; // localize + $infos = &$this->infos; // localize + $tokenIndex = null; + $token = null; + $tokenType = null; + $tokenContent = null; + $tokenLine = null; + $namespace = null; + $infoIndex = 0; + $braceCount = 0; + + /* + * MACRO creation + */ + $MACRO_TOKEN_ADVANCE = function () use (&$tokens, &$tokenIndex, &$token, &$tokenType, &$tokenContent, &$tokenLine) { + static $lastTokenArray = null; + $tokenIndex = ($tokenIndex === null) ? 0 : $tokenIndex + 1; + if (!isset($tokens[$tokenIndex])) { + $token = false; + $tokenContent = false; + $tokenType = false; + $tokenLine = false; + + return false; + } + $token = $tokens[$tokenIndex]; + + if (is_string($token)) { + $tokenType = null; + $tokenContent = $token; + $tokenLine = $tokenLine + substr_count( + $lastTokenArray[1], + "\n" + ); // adjust token line by last known newline count + } else { + $lastTokenArray = $token; + list($tokenType, $tokenContent, $tokenLine) = $token; + } + + return $tokenIndex; + }; + $MACRO_INFO_ADVANCE = function () use (&$infoIndex, &$infos, &$tokenIndex, &$tokenLine) { + $infos[$infoIndex]['tokenEnd'] = $tokenIndex; + $infos[$infoIndex]['lineEnd'] = $tokenLine; + $infoIndex++; + + return $infoIndex; + }; + + /** + * START FINITE STATE MACHINE FOR SCANNING TOKENS + */ + + // Initialize token + $MACRO_TOKEN_ADVANCE(); + + SCANNER_TOP: + + switch ($tokenType) { + + case T_DOC_COMMENT: + + $this->docComment = $tokenContent; + goto SCANNER_CONTINUE; + //goto no break needed + + case T_FINAL: + case T_ABSTRACT: + case T_CLASS: + case T_INTERFACE: + + // CLASS INFORMATION + + $classContext = null; + $classInterfaceIndex = 0; + + SCANNER_CLASS_INFO_TOP: + + if (is_string($tokens[$tokenIndex + 1]) && $tokens[$tokenIndex + 1] === '{') { + goto SCANNER_CLASS_INFO_END; + } + + $this->lineStart = $tokenLine; + + switch ($tokenType) { + + case T_FINAL: + $this->isFinal = true; + goto SCANNER_CLASS_INFO_CONTINUE; + //goto no break needed + + case T_ABSTRACT: + $this->isAbstract = true; + goto SCANNER_CLASS_INFO_CONTINUE; + //goto no break needed + + case T_INTERFACE: + $this->isInterface = true; + //fall-through + case T_CLASS: + $this->shortName = $tokens[$tokenIndex + 2][1]; + if ($this->nameInformation && $this->nameInformation->hasNamespace()) { + $this->name = $this->nameInformation->getNamespace() . '\\' . $this->shortName; + } else { + $this->name = $this->shortName; + } + goto SCANNER_CLASS_INFO_CONTINUE; + //goto no break needed + + case T_NS_SEPARATOR: + case T_STRING: + switch ($classContext) { + case T_EXTENDS: + $this->shortParentClass .= $tokenContent; + break; + case T_IMPLEMENTS: + $this->shortInterfaces[$classInterfaceIndex] .= $tokenContent; + break; + } + goto SCANNER_CLASS_INFO_CONTINUE; + //goto no break needed + + case T_EXTENDS: + case T_IMPLEMENTS: + $classContext = $tokenType; + if (($this->isInterface && $classContext === T_EXTENDS) || $classContext === T_IMPLEMENTS) { + $this->shortInterfaces[$classInterfaceIndex] = ''; + } elseif (!$this->isInterface && $classContext === T_EXTENDS) { + $this->shortParentClass = ''; + } + goto SCANNER_CLASS_INFO_CONTINUE; + //goto no break needed + + case null: + if ($classContext == T_IMPLEMENTS && $tokenContent == ',') { + $classInterfaceIndex++; + $this->shortInterfaces[$classInterfaceIndex] = ''; + } + + } + + SCANNER_CLASS_INFO_CONTINUE: + + if ($MACRO_TOKEN_ADVANCE() === false) { + goto SCANNER_END; + } + goto SCANNER_CLASS_INFO_TOP; + + SCANNER_CLASS_INFO_END: + + goto SCANNER_CONTINUE; + + } + + if ($tokenType === null && $tokenContent === '{' && $braceCount === 0) { + $braceCount++; + if ($MACRO_TOKEN_ADVANCE() === false) { + goto SCANNER_END; + } + + SCANNER_CLASS_BODY_TOP: + + if ($braceCount === 0) { + goto SCANNER_CLASS_BODY_END; + } + + switch ($tokenType) { + + case T_CONST: + + $infos[$infoIndex] = array( + 'type' => 'constant', + 'tokenStart' => $tokenIndex, + 'tokenEnd' => null, + 'lineStart' => $tokenLine, + 'lineEnd' => null, + 'name' => null, + 'value' => null, + ); + + SCANNER_CLASS_BODY_CONST_TOP: + + if ($tokenContent === ';') { + goto SCANNER_CLASS_BODY_CONST_END; + } + + if ($tokenType === T_STRING && null === $infos[$infoIndex]['name']) { + $infos[$infoIndex]['name'] = $tokenContent; + } + + SCANNER_CLASS_BODY_CONST_CONTINUE: + + if ($MACRO_TOKEN_ADVANCE() === false) { + goto SCANNER_END; + } + goto SCANNER_CLASS_BODY_CONST_TOP; + + SCANNER_CLASS_BODY_CONST_END: + + $MACRO_INFO_ADVANCE(); + goto SCANNER_CLASS_BODY_CONTINUE; + //goto no break needed + + case T_DOC_COMMENT: + case T_PUBLIC: + case T_PROTECTED: + case T_PRIVATE: + case T_ABSTRACT: + case T_FINAL: + case T_VAR: + case T_FUNCTION: + + $infos[$infoIndex] = array( + 'type' => null, + 'tokenStart' => $tokenIndex, + 'tokenEnd' => null, + 'lineStart' => $tokenLine, + 'lineEnd' => null, + 'name' => null, + ); + + $memberContext = null; + $methodBodyStarted = false; + + SCANNER_CLASS_BODY_MEMBER_TOP: + + if ($memberContext === 'method') { + switch ($tokenContent) { + case '{': + $methodBodyStarted = true; + $braceCount++; + goto SCANNER_CLASS_BODY_MEMBER_CONTINUE; + //goto no break needed + case '}': + $braceCount--; + goto SCANNER_CLASS_BODY_MEMBER_CONTINUE; + + case ';': + $infos[$infoIndex]['tokenEnd'] = $tokenIndex; + goto SCANNER_CLASS_BODY_MEMBER_CONTINUE; + } + } + + if ($memberContext !== null) { + if ( + ($memberContext === 'property' && $tokenContent === ';') + || ($memberContext === 'method' && $methodBodyStarted && $braceCount === 1) + || ($memberContext === 'method' && $this->isInterface && $tokenContent === ';') + ) { + goto SCANNER_CLASS_BODY_MEMBER_END; + } + } + + switch ($tokenType) { + + case T_CONST: + $memberContext = 'constant'; + $infos[$infoIndex]['type'] = 'constant'; + goto SCANNER_CLASS_BODY_CONST_CONTINUE; + //goto no break needed + + case T_VARIABLE: + if ($memberContext === null) { + $memberContext = 'property'; + $infos[$infoIndex]['type'] = 'property'; + $infos[$infoIndex]['name'] = ltrim($tokenContent, '$'); + } + goto SCANNER_CLASS_BODY_MEMBER_CONTINUE; + //goto no break needed + + case T_FUNCTION: + $memberContext = 'method'; + $infos[$infoIndex]['type'] = 'method'; + goto SCANNER_CLASS_BODY_MEMBER_CONTINUE; + //goto no break needed + + case T_STRING: + if ($memberContext === 'method' && null === $infos[$infoIndex]['name']) { + $infos[$infoIndex]['name'] = $tokenContent; + } + goto SCANNER_CLASS_BODY_MEMBER_CONTINUE; + //goto no break needed + } + + SCANNER_CLASS_BODY_MEMBER_CONTINUE: + + if ($MACRO_TOKEN_ADVANCE() === false) { + goto SCANNER_END; + } + goto SCANNER_CLASS_BODY_MEMBER_TOP; + + SCANNER_CLASS_BODY_MEMBER_END: + + $memberContext = null; + $MACRO_INFO_ADVANCE(); + goto SCANNER_CLASS_BODY_CONTINUE; + //goto no break needed + + case null: // no type, is a string + + switch ($tokenContent) { + case '{': + $braceCount++; + goto SCANNER_CLASS_BODY_CONTINUE; + //fall-through + case '}': + $braceCount--; + goto SCANNER_CLASS_BODY_CONTINUE; + } + } + + SCANNER_CLASS_BODY_CONTINUE: + + if ($braceCount === 0 || $MACRO_TOKEN_ADVANCE() === false) { + goto SCANNER_CONTINUE; + } + goto SCANNER_CLASS_BODY_TOP; + + SCANNER_CLASS_BODY_END: + + goto SCANNER_CONTINUE; + } + + SCANNER_CONTINUE: + + if ($tokenContent === '}') { + $this->lineEnd = $tokenLine; + } + + if ($MACRO_TOKEN_ADVANCE() === false) { + goto SCANNER_END; + } + goto SCANNER_TOP; + + SCANNER_END: + + // process short names + if ($this->nameInformation) { + if ($this->shortParentClass) { + $this->parentClass = $this->nameInformation->resolveName($this->shortParentClass); + } + if ($this->shortInterfaces) { + foreach ($this->shortInterfaces as $siIndex => $si) { + $this->interfaces[$siIndex] = $this->nameInformation->resolveName($si); + } + } + } else { + $this->parentClass = $this->shortParentClass; + $this->interfaces = $this->shortInterfaces; + } + + $this->isScanned = true; + + return; + } +} diff --git a/library/Zend/Code/Scanner/ConstantScanner.php b/library/Zend/Code/Scanner/ConstantScanner.php new file mode 100755 index 0000000000..c37c7a22c6 --- /dev/null +++ b/library/Zend/Code/Scanner/ConstantScanner.php @@ -0,0 +1,236 @@ +tokens = $constantTokens; + $this->nameInformation = $nameInformation; + } + + /** + * @param string $class + */ + public function setClass($class) + { + $this->class = $class; + } + + /** + * @param ClassScanner $scannerClass + */ + public function setScannerClass(ClassScanner $scannerClass) + { + $this->scannerClass = $scannerClass; + } + + /** + * @return ClassScanner + */ + public function getClassScanner() + { + return $this->scannerClass; + } + + /** + * @return string + */ + public function getName() + { + $this->scan(); + return $this->name; + } + + /** + * @return string + */ + public function getValue() + { + $this->scan(); + return $this->value; + } + + /** + * @return string + */ + public function getDocComment() + { + $this->scan(); + return $this->docComment; + } + + /** + * @param Annotation\AnnotationManager $annotationManager + * @return AnnotationScanner + */ + public function getAnnotations(Annotation\AnnotationManager $annotationManager) + { + if (($docComment = $this->getDocComment()) == '') { + return false; + } + + return new AnnotationScanner($annotationManager, $docComment, $this->nameInformation); + } + + /** + * @return string + */ + public function __toString() + { + $this->scan(); + return var_export($this, true); + } + + /** + * Scan tokens + * + * @throws Exception\RuntimeException + */ + protected function scan() + { + if ($this->isScanned) { + return; + } + + if (!$this->tokens) { + throw new Exception\RuntimeException('No tokens were provided'); + } + + /** + * Variables & Setup + */ + $tokens = &$this->tokens; + + reset($tokens); + + SCANNER_TOP: + + $token = current($tokens); + + if (!is_string($token)) { + list($tokenType, $tokenContent, $tokenLine) = $token; + + switch ($tokenType) { + case T_DOC_COMMENT: + if ($this->docComment === null && $this->name === null) { + $this->docComment = $tokenContent; + } + goto SCANNER_CONTINUE; + // fall-through + + case T_STRING: + $string = (is_string($token)) ? $token : $tokenContent; + + if (null === $this->name) { + $this->name = $string; + } else { + if ('self' == strtolower($string)) { + list($tokenNextType, $tokenNextContent, $tokenNextLine) = next($tokens); + + if ('::' == $tokenNextContent) { + list($tokenNextType, $tokenNextContent, $tokenNextLine) = next($tokens); + + if ($this->getClassScanner()->getConstant($tokenNextContent)) { + $this->value = $this->getClassScanner()->getConstant($tokenNextContent)->getValue(); + } + } + } + } + + goto SCANNER_CONTINUE; + // fall-through + + case T_CONSTANT_ENCAPSED_STRING: + case T_DNUMBER: + case T_LNUMBER: + $string = (is_string($token)) ? $token : $tokenContent; + + if (substr($string, 0, 1) === '"' || substr($string, 0, 1) === "'") { + $this->value = substr($string, 1, -1); // Remove quotes + } else { + $this->value = $string; + } + goto SCANNER_CONTINUE; + // fall-trough + + default: + goto SCANNER_CONTINUE; + } + } + + SCANNER_CONTINUE: + + if (next($this->tokens) === false) { + goto SCANNER_END; + } + goto SCANNER_TOP; + + SCANNER_END: + + $this->isScanned = true; + } +} diff --git a/library/Zend/Code/Scanner/DerivedClassScanner.php b/library/Zend/Code/Scanner/DerivedClassScanner.php new file mode 100755 index 0000000000..6c8463ede2 --- /dev/null +++ b/library/Zend/Code/Scanner/DerivedClassScanner.php @@ -0,0 +1,381 @@ +classScanner = $classScanner; + $this->directoryScanner = $directoryScanner; + + $currentScannerClass = $classScanner; + + while ($currentScannerClass && $currentScannerClass->hasParentClass()) { + $currentParentClassName = $currentScannerClass->getParentClass(); + if ($directoryScanner->hasClass($currentParentClassName)) { + $currentParentClass = $directoryScanner->getClass($currentParentClassName); + $this->parentClassScanners[$currentParentClassName] = $currentParentClass; + $currentScannerClass = $currentParentClass; + } else { + $currentScannerClass = false; + } + } + + foreach ($interfaces = $this->classScanner->getInterfaces() as $iName) { + if ($directoryScanner->hasClass($iName)) { + $this->interfaceClassScanners[$iName] = $directoryScanner->getClass($iName); + } + } + } + + /** + * @return null|string + */ + public function getName() + { + return $this->classScanner->getName(); + } + + /** + * @return null|string + */ + public function getShortName() + { + return $this->classScanner->getShortName(); + } + + /** + * @return bool + */ + public function isInstantiable() + { + return $this->classScanner->isInstantiable(); + } + + /** + * @return bool + */ + public function isFinal() + { + return $this->classScanner->isFinal(); + } + + /** + * @return bool + */ + public function isAbstract() + { + return $this->classScanner->isAbstract(); + } + + /** + * @return bool + */ + public function isInterface() + { + return $this->classScanner->isInterface(); + } + + /** + * @return array + */ + public function getParentClasses() + { + return array_keys($this->parentClassScanners); + } + + /** + * @return bool + */ + public function hasParentClass() + { + return ($this->classScanner->getParentClass() != null); + } + + /** + * @return null|string + */ + public function getParentClass() + { + return $this->classScanner->getParentClass(); + } + + /** + * @param bool $returnClassScanners + * @return array + */ + public function getInterfaces($returnClassScanners = false) + { + if ($returnClassScanners) { + return $this->interfaceClassScanners; + } + + $interfaces = $this->classScanner->getInterfaces(); + foreach ($this->parentClassScanners as $pClassScanner) { + $interfaces = array_merge($interfaces, $pClassScanner->getInterfaces()); + } + + return $interfaces; + } + + /** + * Return a list of constant names + * + * @return array + */ + public function getConstantNames() + { + $constants = $this->classScanner->getConstantNames(); + foreach ($this->parentClassScanners as $pClassScanner) { + $constants = array_merge($constants, $pClassScanner->getConstantNames()); + } + + return $constants; + } + + /** + * Return a list of constants + * + * @param bool $namesOnly Set false to return instances of ConstantScanner + * @return array|ConstantScanner[] + */ + public function getConstants($namesOnly = true) + { + if (true === $namesOnly) { + trigger_error('Use method getConstantNames() instead', E_USER_DEPRECATED); + return $this->getConstantNames(); + } + + $constants = $this->classScanner->getConstants(); + foreach ($this->parentClassScanners as $pClassScanner) { + $constants = array_merge($constants, $pClassScanner->getConstants($namesOnly)); + } + + return $constants; + } + + /** + * Return a single constant by given name or index of info + * + * @param string|int $constantNameOrInfoIndex + * @throws Exception\InvalidArgumentException + * @return bool|ConstantScanner + */ + public function getConstant($constantNameOrInfoIndex) + { + if ($this->classScanner->hasConstant($constantNameOrInfoIndex)) { + return $this->classScanner->getConstant($constantNameOrInfoIndex); + } + + foreach ($this->parentClassScanners as $pClassScanner) { + if ($pClassScanner->hasConstant($constantNameOrInfoIndex)) { + return $pClassScanner->getConstant($constantNameOrInfoIndex); + } + } + + throw new Exception\InvalidArgumentException(sprintf( + 'Constant %s not found in %s', + $constantNameOrInfoIndex, + $this->classScanner->getName() + )); + } + + /** + * Verify if class or parent class has constant + * + * @param string $name + * @return bool + */ + public function hasConstant($name) + { + if ($this->classScanner->hasConstant($name)) { + return true; + } + foreach ($this->parentClassScanners as $pClassScanner) { + if ($pClassScanner->hasConstant($name)) { + return true; + } + } + + return false; + } + + /** + * Return a list of property names + * + * @return array + */ + public function getPropertyNames() + { + $properties = $this->classScanner->getPropertyNames(); + foreach ($this->parentClassScanners as $pClassScanner) { + $properties = array_merge($properties, $pClassScanner->getPropertyNames()); + } + + return $properties; + } + + /** + * @param bool $returnScannerProperty + * @return array + */ + public function getProperties($returnScannerProperty = false) + { + $properties = $this->classScanner->getProperties($returnScannerProperty); + foreach ($this->parentClassScanners as $pClassScanner) { + $properties = array_merge($properties, $pClassScanner->getProperties($returnScannerProperty)); + } + + return $properties; + } + + /** + * Return a single property by given name or index of info + * + * @param string|int $propertyNameOrInfoIndex + * @throws Exception\InvalidArgumentException + * @return bool|PropertyScanner + */ + public function getProperty($propertyNameOrInfoIndex) + { + if ($this->classScanner->hasProperty($propertyNameOrInfoIndex)) { + return $this->classScanner->getProperty($propertyNameOrInfoIndex); + } + + foreach ($this->parentClassScanners as $pClassScanner) { + if ($pClassScanner->hasProperty($propertyNameOrInfoIndex)) { + return $pClassScanner->getProperty($propertyNameOrInfoIndex); + } + } + + throw new Exception\InvalidArgumentException(sprintf( + 'Property %s not found in %s', + $propertyNameOrInfoIndex, + $this->classScanner->getName() + )); + } + + /** + * Verify if class or parent class has property + * + * @param string $name + * @return bool + */ + public function hasProperty($name) + { + if ($this->classScanner->hasProperty($name)) { + return true; + } + foreach ($this->parentClassScanners as $pClassScanner) { + if ($pClassScanner->hasProperty($name)) { + return true; + } + } + + return false; + } + + /** + * @return array + */ + public function getMethodNames() + { + $methods = $this->classScanner->getMethodNames(); + foreach ($this->parentClassScanners as $pClassScanner) { + $methods = array_merge($methods, $pClassScanner->getMethodNames()); + } + + return $methods; + } + + /** + * @return MethodScanner[] + */ + public function getMethods() + { + $methods = $this->classScanner->getMethods(); + foreach ($this->parentClassScanners as $pClassScanner) { + $methods = array_merge($methods, $pClassScanner->getMethods()); + } + + return $methods; + } + + /** + * @param int|string $methodNameOrInfoIndex + * @return MethodScanner + * @throws Exception\InvalidArgumentException + */ + public function getMethod($methodNameOrInfoIndex) + { + if ($this->classScanner->hasMethod($methodNameOrInfoIndex)) { + return $this->classScanner->getMethod($methodNameOrInfoIndex); + } + + foreach ($this->parentClassScanners as $pClassScanner) { + if ($pClassScanner->hasMethod($methodNameOrInfoIndex)) { + return $pClassScanner->getMethod($methodNameOrInfoIndex); + } + } + + throw new Exception\InvalidArgumentException(sprintf( + 'Method %s not found in %s', + $methodNameOrInfoIndex, + $this->classScanner->getName() + )); + } + + /** + * Verify if class or parent class has method by given name + * + * @param string $name + * @return bool + */ + public function hasMethod($name) + { + if ($this->classScanner->hasMethod($name)) { + return true; + } + foreach ($this->parentClassScanners as $pClassScanner) { + if ($pClassScanner->hasMethod($name)) { + return true; + } + } + + return false; + } +} diff --git a/library/Zend/Code/Scanner/DirectoryScanner.php b/library/Zend/Code/Scanner/DirectoryScanner.php new file mode 100755 index 0000000000..b1a0223daa --- /dev/null +++ b/library/Zend/Code/Scanner/DirectoryScanner.php @@ -0,0 +1,272 @@ +addDirectory($directory); + } elseif (is_array($directory)) { + foreach ($directory as $d) { + $this->addDirectory($d); + } + } + } + } + + /** + * @param DirectoryScanner|string $directory + * @return void + * @throws Exception\InvalidArgumentException + */ + public function addDirectory($directory) + { + if ($directory instanceof DirectoryScanner) { + $this->directories[] = $directory; + } elseif (is_string($directory)) { + $realDir = realpath($directory); + if (!$realDir || !is_dir($realDir)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Directory "%s" does not exist', + $realDir + )); + } + $this->directories[] = $realDir; + } else { + throw new Exception\InvalidArgumentException( + 'The argument provided was neither a DirectoryScanner or directory path' + ); + } + } + + /** + * @param DirectoryScanner $directoryScanner + * @return void + */ + public function addDirectoryScanner(DirectoryScanner $directoryScanner) + { + $this->addDirectory($directoryScanner); + } + + /** + * @param FileScanner $fileScanner + * @return void + */ + public function addFileScanner(FileScanner $fileScanner) + { + $this->fileScanners[] = $fileScanner; + } + + /** + * @return void + */ + protected function scan() + { + if ($this->isScanned) { + return; + } + + // iterate directories creating file scanners + foreach ($this->directories as $directory) { + if ($directory instanceof DirectoryScanner) { + $directory->scan(); + if ($directory->fileScanners) { + $this->fileScanners = array_merge($this->fileScanners, $directory->fileScanners); + } + } else { + $rdi = new RecursiveDirectoryIterator($directory); + foreach (new RecursiveIteratorIterator($rdi) as $item) { + if ($item->isFile() && pathinfo($item->getRealPath(), PATHINFO_EXTENSION) == 'php') { + $this->fileScanners[] = new FileScanner($item->getRealPath()); + } + } + } + } + + $this->isScanned = true; + } + + /** + * @todo implement method + */ + public function getNamespaces() + { + // @todo + } + + /** + * @param bool $returnFileScanners + * @return array + */ + public function getFiles($returnFileScanners = false) + { + $this->scan(); + + $return = array(); + foreach ($this->fileScanners as $fileScanner) { + $return[] = ($returnFileScanners) ? $fileScanner : $fileScanner->getFile(); + } + + return $return; + } + + /** + * @return array + */ + public function getClassNames() + { + $this->scan(); + + if ($this->classToFileScanner === null) { + $this->createClassToFileScannerCache(); + } + + return array_keys($this->classToFileScanner); + } + + /** + * @param bool $returnDerivedScannerClass + * @return array + */ + public function getClasses($returnDerivedScannerClass = false) + { + $this->scan(); + + if ($this->classToFileScanner === null) { + $this->createClassToFileScannerCache(); + } + + $returnClasses = array(); + foreach ($this->classToFileScanner as $className => $fsIndex) { + $classScanner = $this->fileScanners[$fsIndex]->getClass($className); + if ($returnDerivedScannerClass) { + $classScanner = new DerivedClassScanner($classScanner, $this); + } + $returnClasses[] = $classScanner; + } + + return $returnClasses; + } + + /** + * @param string $class + * @return bool + */ + public function hasClass($class) + { + $this->scan(); + + if ($this->classToFileScanner === null) { + $this->createClassToFileScannerCache(); + } + + return (isset($this->classToFileScanner[$class])); + } + + /** + * @param string $class + * @param bool $returnDerivedScannerClass + * @return ClassScanner|DerivedClassScanner + * @throws Exception\InvalidArgumentException + */ + public function getClass($class, $returnDerivedScannerClass = false) + { + $this->scan(); + + if ($this->classToFileScanner === null) { + $this->createClassToFileScannerCache(); + } + + if (!isset($this->classToFileScanner[$class])) { + throw new Exception\InvalidArgumentException('Class not found.'); + } + + /** @var FileScanner $fs */ + $fs = $this->fileScanners[$this->classToFileScanner[$class]]; + $returnClass = $fs->getClass($class); + + if (($returnClass instanceof ClassScanner) && $returnDerivedScannerClass) { + return new DerivedClassScanner($returnClass, $this); + } + + return $returnClass; + } + + /** + * Create class to file scanner cache + * + * @return void + */ + protected function createClassToFileScannerCache() + { + if ($this->classToFileScanner !== null) { + return; + } + + $this->classToFileScanner = array(); + /** @var FileScanner $fileScanner */ + foreach ($this->fileScanners as $fsIndex => $fileScanner) { + $fsClasses = $fileScanner->getClassNames(); + foreach ($fsClasses as $fsClassName) { + $this->classToFileScanner[$fsClassName] = $fsIndex; + } + } + } + + /** + * Export + * + * @todo implement method + */ + public static function export() + { + // @todo + } + + /** + * __ToString + * + * @todo implement method + */ + public function __toString() + { + // @todo + } +} diff --git a/library/Zend/Code/Scanner/DocBlockScanner.php b/library/Zend/Code/Scanner/DocBlockScanner.php new file mode 100755 index 0000000000..3bb28ece12 --- /dev/null +++ b/library/Zend/Code/Scanner/DocBlockScanner.php @@ -0,0 +1,326 @@ +docComment = $docComment; + $this->nameInformation = $nameInformation; + } + + /** + * @return string + */ + public function getShortDescription() + { + $this->scan(); + + return $this->shortDescription; + } + + /** + * @return string + */ + public function getLongDescription() + { + $this->scan(); + + return $this->longDescription; + } + + /** + * @return array + */ + public function getTags() + { + $this->scan(); + + return $this->tags; + } + + /** + * @return array + */ + public function getAnnotations() + { + $this->scan(); + + return $this->annotations; + } + + /** + * @return void + */ + protected function scan() + { + if ($this->isScanned) { + return; + } + + $mode = 1; + + $tokens = $this->tokenize(); + $tagIndex = null; + reset($tokens); + + SCANNER_TOP: + $token = current($tokens); + + switch ($token[0]) { + case 'DOCBLOCK_NEWLINE': + if ($this->shortDescription != '' && $tagIndex === null) { + $mode = 2; + } else { + $this->longDescription .= $token[1]; + } + goto SCANNER_CONTINUE; + //goto no break needed + + case 'DOCBLOCK_WHITESPACE': + case 'DOCBLOCK_TEXT': + if ($tagIndex !== null) { + $this->tags[$tagIndex]['value'] .= ($this->tags[$tagIndex]['value'] == '') ? $token[1] : ' ' . $token[1]; + goto SCANNER_CONTINUE; + } elseif ($mode <= 2) { + if ($mode == 1) { + $this->shortDescription .= $token[1]; + } else { + $this->longDescription .= $token[1]; + } + goto SCANNER_CONTINUE; + } + //gotos no break needed + case 'DOCBLOCK_TAG': + array_push($this->tags, array('name' => $token[1], + 'value' => '')); + end($this->tags); + $tagIndex = key($this->tags); + $mode = 3; + goto SCANNER_CONTINUE; + //goto no break needed + + case 'DOCBLOCK_COMMENTEND': + goto SCANNER_END; + + } + + SCANNER_CONTINUE: + if (next($tokens) === false) { + goto SCANNER_END; + } + goto SCANNER_TOP; + + SCANNER_END: + + $this->shortDescription = trim($this->shortDescription); + $this->longDescription = trim($this->longDescription); + $this->isScanned = true; + } + + /** + * @return array + */ + protected function tokenize() + { + static $CONTEXT_INSIDE_DOCBLOCK = 0x01; + static $CONTEXT_INSIDE_ASTERISK = 0x02; + + $context = 0x00; + $stream = $this->docComment; + $streamIndex = null; + $tokens = array(); + $tokenIndex = null; + $currentChar = null; + $currentWord = null; + $currentLine = null; + + $MACRO_STREAM_ADVANCE_CHAR = function ($positionsForward = 1) use (&$stream, &$streamIndex, &$currentChar, &$currentWord, &$currentLine) { + $positionsForward = ($positionsForward > 0) ? $positionsForward : 1; + $streamIndex = ($streamIndex === null) ? 0 : $streamIndex + $positionsForward; + if (!isset($stream[$streamIndex])) { + $currentChar = false; + + return false; + } + $currentChar = $stream[$streamIndex]; + $matches = array(); + $currentLine = (preg_match('#(.*?)\r?\n#', $stream, $matches, null, $streamIndex) === 1) ? $matches[1] : substr($stream, $streamIndex); + if ($currentChar === ' ') { + $currentWord = (preg_match('#( +)#', $currentLine, $matches) === 1) ? $matches[1] : $currentLine; + } else { + $currentWord = (($matches = strpos($currentLine, ' ')) !== false) ? substr($currentLine, 0, $matches) : $currentLine; + } + + return $currentChar; + }; + $MACRO_STREAM_ADVANCE_WORD = function () use (&$currentWord, &$MACRO_STREAM_ADVANCE_CHAR) { + return $MACRO_STREAM_ADVANCE_CHAR(strlen($currentWord)); + }; + $MACRO_STREAM_ADVANCE_LINE = function () use (&$currentLine, &$MACRO_STREAM_ADVANCE_CHAR) { + return $MACRO_STREAM_ADVANCE_CHAR(strlen($currentLine)); + }; + $MACRO_TOKEN_ADVANCE = function () use (&$tokenIndex, &$tokens) { + $tokenIndex = ($tokenIndex === null) ? 0 : $tokenIndex + 1; + $tokens[$tokenIndex] = array('DOCBLOCK_UNKNOWN', ''); + }; + $MACRO_TOKEN_SET_TYPE = function ($type) use (&$tokenIndex, &$tokens) { + $tokens[$tokenIndex][0] = $type; + }; + $MACRO_TOKEN_APPEND_CHAR = function () use (&$currentChar, &$tokens, &$tokenIndex) { + $tokens[$tokenIndex][1] .= $currentChar; + }; + $MACRO_TOKEN_APPEND_WORD = function () use (&$currentWord, &$tokens, &$tokenIndex) { + $tokens[$tokenIndex][1] .= $currentWord; + }; + $MACRO_TOKEN_APPEND_WORD_PARTIAL = function ($length) use (&$currentWord, &$tokens, &$tokenIndex) { + $tokens[$tokenIndex][1] .= substr($currentWord, 0, $length); + }; + $MACRO_TOKEN_APPEND_LINE = function () use (&$currentLine, &$tokens, &$tokenIndex) { + $tokens[$tokenIndex][1] .= $currentLine; + }; + + $MACRO_STREAM_ADVANCE_CHAR(); + $MACRO_TOKEN_ADVANCE(); + + TOKENIZER_TOP: + + if ($context === 0x00 && $currentChar === '/' && $currentWord === '/**') { + $MACRO_TOKEN_SET_TYPE('DOCBLOCK_COMMENTSTART'); + $MACRO_TOKEN_APPEND_WORD(); + $MACRO_TOKEN_ADVANCE(); + $context |= $CONTEXT_INSIDE_DOCBLOCK; + $context |= $CONTEXT_INSIDE_ASTERISK; + if ($MACRO_STREAM_ADVANCE_WORD() === false) { + goto TOKENIZER_END; + } + goto TOKENIZER_TOP; + } + + if ($context & $CONTEXT_INSIDE_DOCBLOCK && $currentWord === '*/') { + $MACRO_TOKEN_SET_TYPE('DOCBLOCK_COMMENTEND'); + $MACRO_TOKEN_APPEND_WORD(); + $MACRO_TOKEN_ADVANCE(); + $context &= ~$CONTEXT_INSIDE_DOCBLOCK; + if ($MACRO_STREAM_ADVANCE_WORD() === false) { + goto TOKENIZER_END; + } + goto TOKENIZER_TOP; + } + + if ($currentChar === ' ' || $currentChar === "\t") { + $MACRO_TOKEN_SET_TYPE(($context & $CONTEXT_INSIDE_ASTERISK) ? 'DOCBLOCK_WHITESPACE' : 'DOCBLOCK_WHITESPACE_INDENT'); + $MACRO_TOKEN_APPEND_WORD(); + $MACRO_TOKEN_ADVANCE(); + if ($MACRO_STREAM_ADVANCE_WORD() === false) { + goto TOKENIZER_END; + } + goto TOKENIZER_TOP; + } + + if ($currentChar === '*') { + if (($context & $CONTEXT_INSIDE_DOCBLOCK) && ($context & $CONTEXT_INSIDE_ASTERISK)) { + $MACRO_TOKEN_SET_TYPE('DOCBLOCK_TEXT'); + } else { + $MACRO_TOKEN_SET_TYPE('DOCBLOCK_ASTERISK'); + $context |= $CONTEXT_INSIDE_ASTERISK; + } + $MACRO_TOKEN_APPEND_CHAR(); + $MACRO_TOKEN_ADVANCE(); + if ($MACRO_STREAM_ADVANCE_CHAR() === false) { + goto TOKENIZER_END; + } + goto TOKENIZER_TOP; + } + + if ($currentChar === '@') { + $MACRO_TOKEN_SET_TYPE('DOCBLOCK_TAG'); + $MACRO_TOKEN_APPEND_WORD(); + $MACRO_TOKEN_ADVANCE(); + if ($MACRO_STREAM_ADVANCE_WORD() === false) { + goto TOKENIZER_END; + } + goto TOKENIZER_TOP; + } + + if ($currentChar === "\n") { + $MACRO_TOKEN_SET_TYPE('DOCBLOCK_NEWLINE'); + $MACRO_TOKEN_APPEND_CHAR(); + $MACRO_TOKEN_ADVANCE(); + $context &= ~$CONTEXT_INSIDE_ASTERISK; + if ($MACRO_STREAM_ADVANCE_CHAR() === false) { + goto TOKENIZER_END; + } + goto TOKENIZER_TOP; + } + + $MACRO_TOKEN_SET_TYPE('DOCBLOCK_TEXT'); + $MACRO_TOKEN_APPEND_LINE(); + $MACRO_TOKEN_ADVANCE(); + if ($MACRO_STREAM_ADVANCE_LINE() === false) { + goto TOKENIZER_END; + } + goto TOKENIZER_TOP; + + TOKENIZER_END: + + array_pop($tokens); + + return $tokens; + } +} diff --git a/library/Zend/Code/Scanner/FileScanner.php b/library/Zend/Code/Scanner/FileScanner.php new file mode 100755 index 0000000000..64700f611b --- /dev/null +++ b/library/Zend/Code/Scanner/FileScanner.php @@ -0,0 +1,46 @@ +file = $file; + if (!file_exists($file)) { + throw new Exception\InvalidArgumentException(sprintf( + 'File "%s" not found', + $file + )); + } + parent::__construct(token_get_all(file_get_contents($file)), $annotationManager); + } + + /** + * @return string + */ + public function getFile() + { + return $this->file; + } +} diff --git a/library/Zend/Code/Scanner/FunctionScanner.php b/library/Zend/Code/Scanner/FunctionScanner.php new file mode 100755 index 0000000000..dbb082b23b --- /dev/null +++ b/library/Zend/Code/Scanner/FunctionScanner.php @@ -0,0 +1,16 @@ +tokens = $methodTokens; + $this->nameInformation = $nameInformation; + } + + /** + * @param string $class + * @return MethodScanner + */ + public function setClass($class) + { + $this->class = (string) $class; + return $this; + } + + /** + * @param ClassScanner $scannerClass + * @return MethodScanner + */ + public function setScannerClass(ClassScanner $scannerClass) + { + $this->scannerClass = $scannerClass; + return $this; + } + + /** + * @return MethodScanner + */ + public function getClassScanner() + { + return $this->scannerClass; + } + + /** + * @return string + */ + public function getName() + { + $this->scan(); + + return $this->name; + } + + /** + * @return int + */ + public function getLineStart() + { + $this->scan(); + + return $this->lineStart; + } + + /** + * @return int + */ + public function getLineEnd() + { + $this->scan(); + + return $this->lineEnd; + } + + /** + * @return string + */ + public function getDocComment() + { + $this->scan(); + + return $this->docComment; + } + + /** + * @param AnnotationManager $annotationManager + * @return AnnotationScanner + */ + public function getAnnotations(AnnotationManager $annotationManager) + { + if (($docComment = $this->getDocComment()) == '') { + return false; + } + + return new AnnotationScanner($annotationManager, $docComment, $this->nameInformation); + } + + /** + * @return bool + */ + public function isFinal() + { + $this->scan(); + + return $this->isFinal; + } + + /** + * @return bool + */ + public function isAbstract() + { + $this->scan(); + + return $this->isAbstract; + } + + /** + * @return bool + */ + public function isPublic() + { + $this->scan(); + + return $this->isPublic; + } + + /** + * @return bool + */ + public function isProtected() + { + $this->scan(); + + return $this->isProtected; + } + + /** + * @return bool + */ + public function isPrivate() + { + $this->scan(); + + return $this->isPrivate; + } + + /** + * @return bool + */ + public function isStatic() + { + $this->scan(); + + return $this->isStatic; + } + + /** + * @return int + */ + public function getNumberOfParameters() + { + return count($this->getParameters()); + } + + /** + * @param bool $returnScanner + * @return array + */ + public function getParameters($returnScanner = false) + { + $this->scan(); + + $return = array(); + + foreach ($this->infos as $info) { + if ($info['type'] != 'parameter') { + continue; + } + + if (!$returnScanner) { + $return[] = $info['name']; + } else { + $return[] = $this->getParameter($info['name']); + } + } + + return $return; + } + + /** + * @param int|string $parameterNameOrInfoIndex + * @return ParameterScanner + * @throws Exception\InvalidArgumentException + */ + public function getParameter($parameterNameOrInfoIndex) + { + $this->scan(); + + if (is_int($parameterNameOrInfoIndex)) { + $info = $this->infos[$parameterNameOrInfoIndex]; + if ($info['type'] != 'parameter') { + throw new Exception\InvalidArgumentException('Index of info offset is not about a parameter'); + } + } elseif (is_string($parameterNameOrInfoIndex)) { + foreach ($this->infos as $info) { + if ($info['type'] === 'parameter' && $info['name'] === $parameterNameOrInfoIndex) { + break; + } + unset($info); + } + if (!isset($info)) { + throw new Exception\InvalidArgumentException('Index of info offset is not about a parameter'); + } + } + + $p = new ParameterScanner( + array_slice($this->tokens, $info['tokenStart'], $info['tokenEnd'] - $info['tokenStart']), + $this->nameInformation + ); + $p->setDeclaringFunction($this->name); + $p->setDeclaringScannerFunction($this); + $p->setDeclaringClass($this->class); + $p->setDeclaringScannerClass($this->scannerClass); + $p->setPosition($info['position']); + + return $p; + } + + /** + * @return string + */ + public function getBody() + { + $this->scan(); + + return $this->body; + } + + public static function export() + { + // @todo + } + + public function __toString() + { + $this->scan(); + + return var_export($this, true); + } + + protected function scan() + { + if ($this->isScanned) { + return; + } + + if (!$this->tokens) { + throw new Exception\RuntimeException('No tokens were provided'); + } + + /** + * Variables & Setup + */ + + $tokens = &$this->tokens; // localize + $infos = &$this->infos; // localize + $tokenIndex = null; + $token = null; + $tokenType = null; + $tokenContent = null; + $tokenLine = null; + $infoIndex = 0; + $parentCount = 0; + + /* + * MACRO creation + */ + $MACRO_TOKEN_ADVANCE = function () use (&$tokens, &$tokenIndex, &$token, &$tokenType, &$tokenContent, &$tokenLine) { + static $lastTokenArray = null; + $tokenIndex = ($tokenIndex === null) ? 0 : $tokenIndex + 1; + if (!isset($tokens[$tokenIndex])) { + $token = false; + $tokenContent = false; + $tokenType = false; + $tokenLine = false; + + return false; + } + $token = $tokens[$tokenIndex]; + if (is_string($token)) { + $tokenType = null; + $tokenContent = $token; + $tokenLine = $tokenLine + substr_count( + $lastTokenArray[1], + "\n" + ); // adjust token line by last known newline count + } else { + list($tokenType, $tokenContent, $tokenLine) = $token; + } + + return $tokenIndex; + }; + $MACRO_INFO_START = function () use (&$infoIndex, &$infos, &$tokenIndex, &$tokenLine) { + $infos[$infoIndex] = array( + 'type' => 'parameter', + 'tokenStart' => $tokenIndex, + 'tokenEnd' => null, + 'lineStart' => $tokenLine, + 'lineEnd' => $tokenLine, + 'name' => null, + 'position' => $infoIndex + 1, // position is +1 of infoIndex + ); + }; + $MACRO_INFO_ADVANCE = function () use (&$infoIndex, &$infos, &$tokenIndex, &$tokenLine) { + $infos[$infoIndex]['tokenEnd'] = $tokenIndex; + $infos[$infoIndex]['lineEnd'] = $tokenLine; + $infoIndex++; + + return $infoIndex; + }; + + /** + * START FINITE STATE MACHINE FOR SCANNING TOKENS + */ + + // Initialize token + $MACRO_TOKEN_ADVANCE(); + + SCANNER_TOP: + + $this->lineStart = ($this->lineStart) ? : $tokenLine; + + switch ($tokenType) { + case T_DOC_COMMENT: + $this->lineStart = null; + if ($this->docComment === null && $this->name === null) { + $this->docComment = $tokenContent; + } + goto SCANNER_CONTINUE_SIGNATURE; + //goto (no break needed); + + case T_FINAL: + $this->isFinal = true; + goto SCANNER_CONTINUE_SIGNATURE; + //goto (no break needed); + + case T_ABSTRACT: + $this->isAbstract = true; + goto SCANNER_CONTINUE_SIGNATURE; + //goto (no break needed); + + case T_PUBLIC: + // use defaults + goto SCANNER_CONTINUE_SIGNATURE; + //goto (no break needed); + + case T_PROTECTED: + $this->isProtected = true; + $this->isPublic = false; + goto SCANNER_CONTINUE_SIGNATURE; + //goto (no break needed); + + case T_PRIVATE: + $this->isPrivate = true; + $this->isPublic = false; + goto SCANNER_CONTINUE_SIGNATURE; + //goto (no break needed); + + case T_STATIC: + $this->isStatic = true; + goto SCANNER_CONTINUE_SIGNATURE; + //goto (no break needed); + + case T_VARIABLE: + case T_STRING: + + if ($tokenType === T_STRING && $parentCount === 0) { + $this->name = $tokenContent; + } + + if ($parentCount === 1) { + if (!isset($infos[$infoIndex])) { + $MACRO_INFO_START(); + } + if ($tokenType === T_VARIABLE) { + $infos[$infoIndex]['name'] = ltrim($tokenContent, '$'); + } + } + + goto SCANNER_CONTINUE_SIGNATURE; + //goto (no break needed); + + case null: + + switch ($tokenContent) { + case '&': + if (!isset($infos[$infoIndex])) { + $MACRO_INFO_START(); + } + goto SCANNER_CONTINUE_SIGNATURE; + //goto (no break needed); + case '(': + $parentCount++; + goto SCANNER_CONTINUE_SIGNATURE; + //goto (no break needed); + case ')': + $parentCount--; + if ($parentCount > 0) { + goto SCANNER_CONTINUE_SIGNATURE; + } + if ($parentCount === 0) { + if ($infos) { + $MACRO_INFO_ADVANCE(); + } + $context = 'body'; + } + goto SCANNER_CONTINUE_BODY; + //goto (no break needed); + case ',': + if ($parentCount === 1) { + $MACRO_INFO_ADVANCE(); + } + goto SCANNER_CONTINUE_SIGNATURE; + } + } + + SCANNER_CONTINUE_SIGNATURE: + + if ($MACRO_TOKEN_ADVANCE() === false) { + goto SCANNER_END; + } + goto SCANNER_TOP; + + SCANNER_CONTINUE_BODY: + + $braceCount = 0; + while ($MACRO_TOKEN_ADVANCE() !== false) { + if ($tokenContent == '}') { + $braceCount--; + } + if ($braceCount > 0) { + $this->body .= $tokenContent; + } + if ($tokenContent == '{') { + $braceCount++; + } + $this->lineEnd = $tokenLine; + } + + SCANNER_END: + + $this->isScanned = true; + + return; + } +} diff --git a/library/Zend/Code/Scanner/ParameterScanner.php b/library/Zend/Code/Scanner/ParameterScanner.php new file mode 100755 index 0000000000..ef15a16c87 --- /dev/null +++ b/library/Zend/Code/Scanner/ParameterScanner.php @@ -0,0 +1,352 @@ +tokens = $parameterTokens; + $this->nameInformation = $nameInformation; + } + + /** + * Set declaring class + * + * @param string $class + * @return void + */ + public function setDeclaringClass($class) + { + $this->declaringClass = (string) $class; + } + + /** + * Set declaring scanner class + * + * @param ClassScanner $scannerClass + * @return void + */ + public function setDeclaringScannerClass(ClassScanner $scannerClass) + { + $this->declaringScannerClass = $scannerClass; + } + + /** + * Set declaring function + * + * @param string $function + * @return void + */ + public function setDeclaringFunction($function) + { + $this->declaringFunction = $function; + } + + /** + * Set declaring scanner function + * + * @param MethodScanner $scannerFunction + * @return void + */ + public function setDeclaringScannerFunction(MethodScanner $scannerFunction) + { + $this->declaringScannerFunction = $scannerFunction; + } + + /** + * Set position + * + * @param int $position + * @return void + */ + public function setPosition($position) + { + $this->position = $position; + } + + /** + * Scan + * + * @return void + */ + protected function scan() + { + if ($this->isScanned) { + return; + } + + $tokens = &$this->tokens; + + reset($tokens); + + SCANNER_TOP: + + $token = current($tokens); + + if (is_string($token)) { + // check pass by ref + if ($token === '&') { + $this->isPassedByReference = true; + goto SCANNER_CONTINUE; + } + if ($token === '=') { + $this->isOptional = true; + $this->isDefaultValueAvailable = true; + goto SCANNER_CONTINUE; + } + } else { + if ($this->name === null && ($token[0] === T_STRING || $token[0] === T_NS_SEPARATOR)) { + $this->class .= $token[1]; + goto SCANNER_CONTINUE; + } + if ($token[0] === T_VARIABLE) { + $this->name = ltrim($token[1], '$'); + goto SCANNER_CONTINUE; + } + } + + if ($this->name !== null) { + $this->defaultValue .= trim((is_string($token)) ? $token : $token[1]); + } + + SCANNER_CONTINUE: + + if (next($this->tokens) === false) { + goto SCANNER_END; + } + goto SCANNER_TOP; + + SCANNER_END: + + if ($this->class && $this->nameInformation) { + $this->class = $this->nameInformation->resolveName($this->class); + } + + $this->isScanned = true; + } + + /** + * Get declaring scanner class + * + * @return ClassScanner + */ + public function getDeclaringScannerClass() + { + return $this->declaringScannerClass; + } + + /** + * Get declaring class + * + * @return string + */ + public function getDeclaringClass() + { + return $this->declaringClass; + } + + /** + * Get declaring scanner function + * + * @return MethodScanner + */ + public function getDeclaringScannerFunction() + { + return $this->declaringScannerFunction; + } + + /** + * Get declaring function + * + * @return string + */ + public function getDeclaringFunction() + { + return $this->declaringFunction; + } + + /** + * Get default value + * + * @return string + */ + public function getDefaultValue() + { + $this->scan(); + + return $this->defaultValue; + } + + /** + * Get class + * + * @return string + */ + public function getClass() + { + $this->scan(); + + return $this->class; + } + + /** + * Get name + * + * @return string + */ + public function getName() + { + $this->scan(); + + return $this->name; + } + + /** + * Get position + * + * @return int + */ + public function getPosition() + { + $this->scan(); + + return $this->position; + } + + /** + * Check if is array + * + * @return bool + */ + public function isArray() + { + $this->scan(); + + return $this->isArray; + } + + /** + * Check if default value is available + * + * @return bool + */ + public function isDefaultValueAvailable() + { + $this->scan(); + + return $this->isDefaultValueAvailable; + } + + /** + * Check if is optional + * + * @return bool + */ + public function isOptional() + { + $this->scan(); + + return $this->isOptional; + } + + /** + * Check if is passed by reference + * + * @return bool + */ + public function isPassedByReference() + { + $this->scan(); + + return $this->isPassedByReference; + } +} diff --git a/library/Zend/Code/Scanner/PropertyScanner.php b/library/Zend/Code/Scanner/PropertyScanner.php new file mode 100755 index 0000000000..ce38b15856 --- /dev/null +++ b/library/Zend/Code/Scanner/PropertyScanner.php @@ -0,0 +1,316 @@ +tokens = $propertyTokens; + $this->nameInformation = $nameInformation; + } + + /** + * @param string $class + */ + public function setClass($class) + { + $this->class = $class; + } + + /** + * @param ClassScanner $scannerClass + */ + public function setScannerClass(ClassScanner $scannerClass) + { + $this->scannerClass = $scannerClass; + } + + /** + * @return ClassScanner + */ + public function getClassScanner() + { + return $this->scannerClass; + } + + /** + * @return string + */ + public function getName() + { + $this->scan(); + return $this->name; + } + + /** + * @return string + */ + public function getValueType() + { + return $this->valueType; + } + + /** + * @return bool + */ + public function isPublic() + { + $this->scan(); + return $this->isPublic; + } + + /** + * @return bool + */ + public function isPrivate() + { + $this->scan(); + return $this->isPrivate; + } + + /** + * @return bool + */ + public function isProtected() + { + $this->scan(); + return $this->isProtected; + } + + /** + * @return bool + */ + public function isStatic() + { + $this->scan(); + return $this->isStatic; + } + + /** + * @return string + */ + public function getValue() + { + $this->scan(); + return $this->value; + } + + /** + * @return string + */ + public function getDocComment() + { + $this->scan(); + return $this->docComment; + } + + /** + * @param Annotation\AnnotationManager $annotationManager + * @return AnnotationScanner + */ + public function getAnnotations(Annotation\AnnotationManager $annotationManager) + { + if (($docComment = $this->getDocComment()) == '') { + return false; + } + + return new AnnotationScanner($annotationManager, $docComment, $this->nameInformation); + } + + /** + * @return string + */ + public function __toString() + { + $this->scan(); + return var_export($this, true); + } + + /** + * Scan tokens + * + * @throws \Zend\Code\Exception\RuntimeException + */ + protected function scan() + { + if ($this->isScanned) { + return; + } + + if (!$this->tokens) { + throw new Exception\RuntimeException('No tokens were provided'); + } + + /** + * Variables & Setup + */ + $value = ''; + $concatenateValue = false; + + $tokens = &$this->tokens; + reset($tokens); + + foreach ($tokens as $token) { + $tempValue = $token; + if (!is_string($token)) { + list($tokenType, $tokenContent, $tokenLine) = $token; + + switch ($tokenType) { + case T_DOC_COMMENT: + if ($this->docComment === null && $this->name === null) { + $this->docComment = $tokenContent; + } + break; + + case T_VARIABLE: + $this->name = ltrim($tokenContent, '$'); + break; + + case T_PUBLIC: + // use defaults + break; + + case T_PROTECTED: + $this->isProtected = true; + $this->isPublic = false; + break; + + case T_PRIVATE: + $this->isPrivate = true; + $this->isPublic = false; + break; + + case T_STATIC: + $this->isStatic = true; + break; + default: + $tempValue = trim($tokenContent); + break; + } + } + + //end value concatenation + if (!is_array($token) && trim($token) == ";") { + $concatenateValue = false; + } + + if (true === $concatenateValue) { + $value .= $tempValue; + } + + //start value concatenation + if (!is_array($token) && trim($token) == "=") { + $concatenateValue = true; + } + } + + $this->valueType = self::T_UNKNOWN; + if ($value == "false" || $value == "true") { + $this->valueType = self::T_BOOLEAN; + } elseif (is_numeric($value)) { + $this->valueType = self::T_INTEGER; + } elseif (0 === strpos($value, 'array') || 0 === strpos($value, "[")) { + $this->valueType = self::T_ARRAY; + } elseif (substr($value, 0, 1) === '"' || substr($value, 0, 1) === "'") { + $value = substr($value, 1, -1); // Remove quotes + $this->valueType = self::T_STRING; + } + + $this->value = empty($value) ? null : $value; + $this->isScanned = true; + } +} diff --git a/library/Zend/Code/Scanner/ScannerInterface.php b/library/Zend/Code/Scanner/ScannerInterface.php new file mode 100755 index 0000000000..8acb04ecd4 --- /dev/null +++ b/library/Zend/Code/Scanner/ScannerInterface.php @@ -0,0 +1,16 @@ +tokens = $tokens; + $this->annotationManager = $annotationManager; + } + + /** + * @return AnnotationManager + */ + public function getAnnotationManager() + { + return $this->annotationManager; + } + + /** + * Get doc comment + * + * @todo Assignment of $this->docComment should probably be done in scan() + * and then $this->getDocComment() just retrieves it. + * + * @return string + */ + public function getDocComment() + { + foreach ($this->tokens as $token) { + $type = $token[0]; + $value = $token[1]; + if (($type == T_OPEN_TAG) || ($type == T_WHITESPACE)) { + continue; + } elseif ($type == T_DOC_COMMENT) { + $this->docComment = $value; + + return $this->docComment; + } else { + // Only whitespace is allowed before file docblocks + return; + } + } + } + + /** + * @return array + */ + public function getNamespaces() + { + $this->scan(); + + $namespaces = array(); + foreach ($this->infos as $info) { + if ($info['type'] == 'namespace') { + $namespaces[] = $info['namespace']; + } + } + + return $namespaces; + } + + /** + * @param null|string $namespace + * @return array|null + */ + public function getUses($namespace = null) + { + $this->scan(); + + return $this->getUsesNoScan($namespace); + } + + /** + * @return array + */ + public function getIncludes() + { + $this->scan(); + // @todo Implement getIncludes() in TokenArrayScanner + } + + /** + * @return array + */ + public function getClassNames() + { + $this->scan(); + + $return = array(); + foreach ($this->infos as $info) { + if ($info['type'] != 'class') { + continue; + } + + $return[] = $info['name']; + } + + return $return; + } + + /** + * @return ClassScanner[] + */ + public function getClasses() + { + $this->scan(); + + $return = array(); + foreach ($this->infos as $info) { + if ($info['type'] != 'class') { + continue; + } + + $return[] = $this->getClass($info['name']); + } + + return $return; + } + + /** + * Return the class object from this scanner + * + * @param string|int $name + * @throws Exception\InvalidArgumentException + * @return ClassScanner + */ + public function getClass($name) + { + $this->scan(); + + if (is_int($name)) { + $info = $this->infos[$name]; + if ($info['type'] != 'class') { + throw new Exception\InvalidArgumentException('Index of info offset is not about a class'); + } + } elseif (is_string($name)) { + $classFound = false; + foreach ($this->infos as $info) { + if ($info['type'] === 'class' && $info['name'] === $name) { + $classFound = true; + break; + } + } + + if (!$classFound) { + return false; + } + } + + return new ClassScanner( + array_slice( + $this->tokens, + $info['tokenStart'], + ($info['tokenEnd'] - $info['tokenStart'] + 1) + ), // zero indexed array + new NameInformation($info['namespace'], $info['uses']) + ); + } + + /** + * @param string $className + * @return bool|null|NameInformation + */ + public function getClassNameInformation($className) + { + $this->scan(); + + $classFound = false; + foreach ($this->infos as $info) { + if ($info['type'] === 'class' && $info['name'] === $className) { + $classFound = true; + break; + } + } + + if (!$classFound) { + return false; + } + + if (!isset($info)) { + return null; + } + + return new NameInformation($info['namespace'], $info['uses']); + } + + /** + * @return array + */ + public function getFunctionNames() + { + $this->scan(); + $functionNames = array(); + foreach ($this->infos as $info) { + if ($info['type'] == 'function') { + $functionNames[] = $info['name']; + } + } + + return $functionNames; + } + + /** + * @return array + */ + public function getFunctions() + { + $this->scan(); + + $functions = array(); + foreach ($this->infos as $info) { + if ($info['type'] == 'function') { + // @todo $functions[] = new FunctionScanner($info['name']); + } + } + + return $functions; + } + + /** + * Export + * + * @param $tokens + */ + public static function export($tokens) + { + // @todo + } + + public function __toString() + { + // @todo + } + + /** + * Scan + * + * @todo: $this->docComment should be assigned for valid docblock during + * the scan instead of $this->getDocComment() (starting with + * T_DOC_COMMENT case) + * + * @throws Exception\RuntimeException + */ + protected function scan() + { + if ($this->isScanned) { + return; + } + + if (!$this->tokens) { + throw new Exception\RuntimeException('No tokens were provided'); + } + + /** + * Define PHP 5.4 'trait' token constant. + */ + if (!defined('T_TRAIT')) { + define('T_TRAIT', 42001); + } + + /** + * Variables & Setup + */ + + $tokens = &$this->tokens; // localize + $infos = &$this->infos; // localize + $tokenIndex = null; + $token = null; + $tokenType = null; + $tokenContent = null; + $tokenLine = null; + $namespace = null; + $docCommentIndex = false; + $infoIndex = 0; + + /* + * MACRO creation + */ + $MACRO_TOKEN_ADVANCE = function () use (&$tokens, &$tokenIndex, &$token, &$tokenType, &$tokenContent, &$tokenLine) { + $tokenIndex = ($tokenIndex === null) ? 0 : $tokenIndex + 1; + if (!isset($tokens[$tokenIndex])) { + $token = false; + $tokenContent = false; + $tokenType = false; + $tokenLine = false; + + return false; + } + if (is_string($tokens[$tokenIndex]) && $tokens[$tokenIndex] === '"') { + do { + $tokenIndex++; + } while (!(is_string($tokens[$tokenIndex]) && $tokens[$tokenIndex] === '"')); + } + $token = $tokens[$tokenIndex]; + if (is_array($token)) { + list($tokenType, $tokenContent, $tokenLine) = $token; + } else { + $tokenType = null; + $tokenContent = $token; + } + + return $tokenIndex; + }; + $MACRO_TOKEN_LOGICAL_START_INDEX = function () use (&$tokenIndex, &$docCommentIndex) { + return ($docCommentIndex === false) ? $tokenIndex : $docCommentIndex; + }; + $MACRO_DOC_COMMENT_START = function () use (&$tokenIndex, &$docCommentIndex) { + $docCommentIndex = $tokenIndex; + + return $docCommentIndex; + }; + $MACRO_DOC_COMMENT_VALIDATE = function () use (&$tokenType, &$docCommentIndex) { + static $validTrailingTokens = null; + if ($validTrailingTokens === null) { + $validTrailingTokens = array(T_WHITESPACE, T_FINAL, T_ABSTRACT, T_INTERFACE, T_CLASS, T_FUNCTION); + } + if ($docCommentIndex !== false && !in_array($tokenType, $validTrailingTokens)) { + $docCommentIndex = false; + } + + return $docCommentIndex; + }; + $MACRO_INFO_ADVANCE = function () use (&$infoIndex, &$infos, &$tokenIndex, &$tokenLine) { + $infos[$infoIndex]['tokenEnd'] = $tokenIndex; + $infos[$infoIndex]['lineEnd'] = $tokenLine; + $infoIndex++; + + return $infoIndex; + }; + + /** + * START FINITE STATE MACHINE FOR SCANNING TOKENS + */ + + // Initialize token + $MACRO_TOKEN_ADVANCE(); + + SCANNER_TOP: + + if ($token === false) { + goto SCANNER_END; + } + + // Validate current doc comment index + $MACRO_DOC_COMMENT_VALIDATE(); + + switch ($tokenType) { + + case T_DOC_COMMENT: + + $MACRO_DOC_COMMENT_START(); + goto SCANNER_CONTINUE; + //goto no break needed + + case T_NAMESPACE: + + $infos[$infoIndex] = array( + 'type' => 'namespace', + 'tokenStart' => $MACRO_TOKEN_LOGICAL_START_INDEX(), + 'tokenEnd' => null, + 'lineStart' => $token[2], + 'lineEnd' => null, + 'namespace' => null, + ); + + // start processing with next token + if ($MACRO_TOKEN_ADVANCE() === false) { + goto SCANNER_END; + } + + SCANNER_NAMESPACE_TOP: + + if ($tokenType === null && $tokenContent === ';' || $tokenContent === '{') { + goto SCANNER_NAMESPACE_END; + } + + if ($tokenType === T_WHITESPACE) { + goto SCANNER_NAMESPACE_CONTINUE; + } + + if ($tokenType === T_NS_SEPARATOR || $tokenType === T_STRING) { + $infos[$infoIndex]['namespace'] .= $tokenContent; + } + + SCANNER_NAMESPACE_CONTINUE: + + if ($MACRO_TOKEN_ADVANCE() === false) { + goto SCANNER_END; + } + goto SCANNER_NAMESPACE_TOP; + + SCANNER_NAMESPACE_END: + + $namespace = $infos[$infoIndex]['namespace']; + + $MACRO_INFO_ADVANCE(); + goto SCANNER_CONTINUE; + //goto no break needed + + case T_USE: + + $infos[$infoIndex] = array( + 'type' => 'use', + 'tokenStart' => $MACRO_TOKEN_LOGICAL_START_INDEX(), + 'tokenEnd' => null, + 'lineStart' => $tokens[$tokenIndex][2], + 'lineEnd' => null, + 'namespace' => $namespace, + 'statements' => array(0 => array('use' => null, + 'as' => null)), + ); + + $useStatementIndex = 0; + $useAsContext = false; + + // start processing with next token + if ($MACRO_TOKEN_ADVANCE() === false) { + goto SCANNER_END; + } + + SCANNER_USE_TOP: + + if ($tokenType === null) { + if ($tokenContent === ';') { + goto SCANNER_USE_END; + } elseif ($tokenContent === ',') { + $useAsContext = false; + $useStatementIndex++; + $infos[$infoIndex]['statements'][$useStatementIndex] = array('use' => null, + 'as' => null); + } + } + + // ANALYZE + if ($tokenType !== null) { + if ($tokenType == T_AS) { + $useAsContext = true; + goto SCANNER_USE_CONTINUE; + } + + if ($tokenType == T_NS_SEPARATOR || $tokenType == T_STRING) { + if ($useAsContext == false) { + $infos[$infoIndex]['statements'][$useStatementIndex]['use'] .= $tokenContent; + } else { + $infos[$infoIndex]['statements'][$useStatementIndex]['as'] = $tokenContent; + } + } + } + + SCANNER_USE_CONTINUE: + + if ($MACRO_TOKEN_ADVANCE() === false) { + goto SCANNER_END; + } + goto SCANNER_USE_TOP; + + SCANNER_USE_END: + + $MACRO_INFO_ADVANCE(); + goto SCANNER_CONTINUE; + //goto no break needed + + case T_INCLUDE: + case T_INCLUDE_ONCE: + case T_REQUIRE: + case T_REQUIRE_ONCE: + + // Static for performance + static $includeTypes = array( + T_INCLUDE => 'include', + T_INCLUDE_ONCE => 'include_once', + T_REQUIRE => 'require', + T_REQUIRE_ONCE => 'require_once' + ); + + $infos[$infoIndex] = array( + 'type' => 'include', + 'tokenStart' => $MACRO_TOKEN_LOGICAL_START_INDEX(), + 'tokenEnd' => null, + 'lineStart' => $tokens[$tokenIndex][2], + 'lineEnd' => null, + 'includeType' => $includeTypes[$tokens[$tokenIndex][0]], + 'path' => '', + ); + + // start processing with next token + if ($MACRO_TOKEN_ADVANCE() === false) { + goto SCANNER_END; + } + + SCANNER_INCLUDE_TOP: + + if ($tokenType === null && $tokenContent === ';') { + goto SCANNER_INCLUDE_END; + } + + $infos[$infoIndex]['path'] .= $tokenContent; + + SCANNER_INCLUDE_CONTINUE: + + if ($MACRO_TOKEN_ADVANCE() === false) { + goto SCANNER_END; + } + goto SCANNER_INCLUDE_TOP; + + SCANNER_INCLUDE_END: + + $MACRO_INFO_ADVANCE(); + goto SCANNER_CONTINUE; + //goto no break needed + + case T_FUNCTION: + case T_FINAL: + case T_ABSTRACT: + case T_CLASS: + case T_INTERFACE: + case T_TRAIT: + + $infos[$infoIndex] = array( + 'type' => ($tokenType === T_FUNCTION) ? 'function' : 'class', + 'tokenStart' => $MACRO_TOKEN_LOGICAL_START_INDEX(), + 'tokenEnd' => null, + 'lineStart' => $tokens[$tokenIndex][2], + 'lineEnd' => null, + 'namespace' => $namespace, + 'uses' => $this->getUsesNoScan($namespace), + 'name' => null, + 'shortName' => null, + ); + + $classBraceCount = 0; + + // start processing with current token + + SCANNER_CLASS_TOP: + + // process the name + if ($infos[$infoIndex]['shortName'] == '' + && (($tokenType === T_CLASS || $tokenType === T_INTERFACE || $tokenType === T_TRAIT) && $infos[$infoIndex]['type'] === 'class' + || ($tokenType === T_FUNCTION && $infos[$infoIndex]['type'] === 'function')) + ) { + $infos[$infoIndex]['shortName'] = $tokens[$tokenIndex + 2][1]; + $infos[$infoIndex]['name'] = (($namespace != null) ? $namespace . '\\' : '') . $infos[$infoIndex]['shortName']; + } + + if ($tokenType === null) { + if ($tokenContent == '{') { + $classBraceCount++; + } + if ($tokenContent == '}') { + $classBraceCount--; + if ($classBraceCount === 0) { + goto SCANNER_CLASS_END; + } + } + } + + SCANNER_CLASS_CONTINUE: + + if ($MACRO_TOKEN_ADVANCE() === false) { + goto SCANNER_END; + } + goto SCANNER_CLASS_TOP; + + SCANNER_CLASS_END: + + $MACRO_INFO_ADVANCE(); + goto SCANNER_CONTINUE; + + } + + SCANNER_CONTINUE: + + if ($MACRO_TOKEN_ADVANCE() === false) { + goto SCANNER_END; + } + goto SCANNER_TOP; + + SCANNER_END: + + /** + * END FINITE STATE MACHINE FOR SCANNING TOKENS + */ + + $this->isScanned = true; + } + + /** + * Check for namespace + * + * @param string $namespace + * @return bool + */ + public function hasNamespace($namespace) + { + $this->scan(); + + foreach ($this->infos as $info) { + if ($info['type'] == 'namespace' && $info['namespace'] == $namespace) { + return true; + } + } + return false; + } + + /** + * @param string $namespace + * @return null|array + * @throws Exception\InvalidArgumentException + */ + protected function getUsesNoScan($namespace) + { + $namespaces = array(); + foreach ($this->infos as $info) { + if ($info['type'] == 'namespace') { + $namespaces[] = $info['namespace']; + } + } + + if ($namespace === null) { + $namespace = array_shift($namespaces); + } elseif (!is_string($namespace)) { + throw new Exception\InvalidArgumentException('Invalid namespace provided'); + } elseif (!in_array($namespace, $namespaces)) { + return null; + } + + $uses = array(); + foreach ($this->infos as $info) { + if ($info['type'] !== 'use') { + continue; + } + foreach ($info['statements'] as $statement) { + if ($info['namespace'] == $namespace) { + $uses[] = $statement; + } + } + } + + return $uses; + } +} diff --git a/library/Zend/Code/Scanner/Util.php b/library/Zend/Code/Scanner/Util.php new file mode 100755 index 0000000000..e54ae0b883 --- /dev/null +++ b/library/Zend/Code/Scanner/Util.php @@ -0,0 +1,74 @@ +namespace && !$data->uses && strlen($value) > 0 && $value{0} != '\\') { + $value = $data->namespace . '\\' . $value; + + return; + } + + if (!$data->uses || strlen($value) <= 0 || $value{0} == '\\') { + $value = ltrim($value, '\\'); + + return; + } + + if ($data->namespace || $data->uses) { + $firstPart = $value; + if (($firstPartEnd = strpos($firstPart, '\\')) !== false) { + $firstPart = substr($firstPart, 0, $firstPartEnd); + } else { + $firstPartEnd = strlen($firstPart); + } + + if (array_key_exists($firstPart, $data->uses)) { + $value = substr_replace($value, $data->uses[$firstPart], 0, $firstPartEnd); + + return; + } + + if ($data->namespace) { + $value = $data->namespace . '\\' . $value; + + return; + } + } + } +} diff --git a/library/Zend/Code/Scanner/ValueScanner.php b/library/Zend/Code/Scanner/ValueScanner.php new file mode 100755 index 0000000000..d0826cc245 --- /dev/null +++ b/library/Zend/Code/Scanner/ValueScanner.php @@ -0,0 +1,15 @@ +=5.3.23", + "zendframework/zend-eventmanager": "self.version" + }, + "require-dev": { + "doctrine/common": ">=2.1", + "zendframework/zend-stdlib": "self.version" + }, + "suggest": { + "doctrine/common": "Doctrine\\Common >=2.1 for annotation features", + "zendframework/zend-stdlib": "Zend\\Stdlib component" + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev", + "dev-develop": "2.4-dev" + } + } +} diff --git a/library/Zend/Config/AbstractConfigFactory.php b/library/Zend/Config/AbstractConfigFactory.php new file mode 100755 index 0000000000..20f04be024 --- /dev/null +++ b/library/Zend/Config/AbstractConfigFactory.php @@ -0,0 +1,172 @@ +configs[$requestedName])) { + return true; + } + + if (!$serviceLocator->has('Config')) { + return false; + } + + $key = $this->match($requestedName); + if (null === $key) { + return false; + } + + $config = $serviceLocator->get('Config'); + return isset($config[$key]); + } + + /** + * Create service with name + * + * @param ServiceManager\ServiceLocatorInterface $serviceLocator + * @param string $name + * @param string $requestedName + * @return string|mixed|array + */ + public function createServiceWithName(ServiceManager\ServiceLocatorInterface $serviceLocator, $name, $requestedName) + { + if (isset($this->configs[$requestedName])) { + return $this->configs[$requestedName]; + } + + $key = $this->match($requestedName); + if (isset($this->configs[$key])) { + $this->configs[$requestedName] = $this->configs[$key]; + return $this->configs[$key]; + } + + $config = $serviceLocator->get('Config'); + $this->configs[$requestedName] = $this->configs[$key] = $config[$key]; + return $config; + } + + /** + * @param string $pattern + * @return self + * @throws Exception\InvalidArgumentException + */ + public function addPattern($pattern) + { + if (!is_string($pattern)) { + throw new Exception\InvalidArgumentException('pattern must be string'); + } + + $patterns = $this->getPatterns(); + array_unshift($patterns, $pattern); + $this->setPatterns($patterns); + return $this; + } + + /** + * @param array|Traversable $patterns + * @return self + * @throws Exception\InvalidArgumentException + */ + public function addPatterns($patterns) + { + if ($patterns instanceof Traversable) { + $patterns = iterator_to_array($patterns); + } + + if (!is_array($patterns)) { + throw new Exception\InvalidArgumentException("patterns must be array or Traversable"); + } + + foreach ($patterns as $pattern) { + $this->addPattern($pattern); + } + + return $this; + } + + /** + * @param array|Traversable $patterns + * @return self + * @throws \InvalidArgumentException + */ + public function setPatterns($patterns) + { + if ($patterns instanceof Traversable) { + $patterns = iterator_to_array($patterns); + } + + if (!is_array($patterns)) { + throw new \InvalidArgumentException("patterns must be array or Traversable"); + } + + $this->patterns = $patterns; + return $this; + } + + /** + * @return array + */ + public function getPatterns() + { + if (null === $this->patterns) { + $this->setPatterns($this->defaultPatterns); + } + return $this->patterns; + } + + /** + * @param string $requestedName + * @return null|string + */ + protected function match($requestedName) + { + foreach ($this->getPatterns() as $pattern) { + if (preg_match($pattern, $requestedName, $matches)) { + return $matches[1]; + } + } + return null; + } +} diff --git a/library/Zend/Config/CONTRIBUTING.md b/library/Zend/Config/CONTRIBUTING.md new file mode 100755 index 0000000000..e77f5d2d5b --- /dev/null +++ b/library/Zend/Config/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# CONTRIBUTING + +Please don't open pull requests against this repository, please use https://github.com/zendframework/zf2. \ No newline at end of file diff --git a/library/Zend/Config/Config.php b/library/Zend/Config/Config.php new file mode 100755 index 0000000000..17b6797820 --- /dev/null +++ b/library/Zend/Config/Config.php @@ -0,0 +1,400 @@ +allowModifications = (bool) $allowModifications; + + foreach ($array as $key => $value) { + if (is_array($value)) { + $this->data[$key] = new static($value, $this->allowModifications); + } else { + $this->data[$key] = $value; + } + + $this->count++; + } + } + + /** + * Retrieve a value and return $default if there is no element set. + * + * @param string $name + * @param mixed $default + * @return mixed + */ + public function get($name, $default = null) + { + if (array_key_exists($name, $this->data)) { + return $this->data[$name]; + } + + return $default; + } + + /** + * Magic function so that $obj->value will work. + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + return $this->get($name); + } + + /** + * Set a value in the config. + * + * Only allow setting of a property if $allowModifications was set to true + * on construction. Otherwise, throw an exception. + * + * @param string $name + * @param mixed $value + * @return void + * @throws Exception\RuntimeException + */ + public function __set($name, $value) + { + if ($this->allowModifications) { + if (is_array($value)) { + $value = new static($value, true); + } + + if (null === $name) { + $this->data[] = $value; + } else { + $this->data[$name] = $value; + } + + $this->count++; + } else { + throw new Exception\RuntimeException('Config is read only'); + } + } + + /** + * Deep clone of this instance to ensure that nested Zend\Configs are also + * cloned. + * + * @return void + */ + public function __clone() + { + $array = array(); + + foreach ($this->data as $key => $value) { + if ($value instanceof self) { + $array[$key] = clone $value; + } else { + $array[$key] = $value; + } + } + + $this->data = $array; + } + + /** + * Return an associative array of the stored data. + * + * @return array + */ + public function toArray() + { + $array = array(); + $data = $this->data; + + /** @var self $value */ + foreach ($data as $key => $value) { + if ($value instanceof self) { + $array[$key] = $value->toArray(); + } else { + $array[$key] = $value; + } + } + + return $array; + } + + /** + * isset() overloading + * + * @param string $name + * @return bool + */ + public function __isset($name) + { + return isset($this->data[$name]); + } + + /** + * unset() overloading + * + * @param string $name + * @return void + * @throws Exception\InvalidArgumentException + */ + public function __unset($name) + { + if (!$this->allowModifications) { + throw new Exception\InvalidArgumentException('Config is read only'); + } elseif (isset($this->data[$name])) { + unset($this->data[$name]); + $this->count--; + $this->skipNextIteration = true; + } + } + + /** + * count(): defined by Countable interface. + * + * @see Countable::count() + * @return int + */ + public function count() + { + return $this->count; + } + + /** + * current(): defined by Iterator interface. + * + * @see Iterator::current() + * @return mixed + */ + public function current() + { + $this->skipNextIteration = false; + return current($this->data); + } + + /** + * key(): defined by Iterator interface. + * + * @see Iterator::key() + * @return mixed + */ + public function key() + { + return key($this->data); + } + + /** + * next(): defined by Iterator interface. + * + * @see Iterator::next() + * @return void + */ + public function next() + { + if ($this->skipNextIteration) { + $this->skipNextIteration = false; + return; + } + + next($this->data); + } + + /** + * rewind(): defined by Iterator interface. + * + * @see Iterator::rewind() + * @return void + */ + public function rewind() + { + $this->skipNextIteration = false; + reset($this->data); + } + + /** + * valid(): defined by Iterator interface. + * + * @see Iterator::valid() + * @return bool + */ + public function valid() + { + return ($this->key() !== null); + } + + /** + * offsetExists(): defined by ArrayAccess interface. + * + * @see ArrayAccess::offsetExists() + * @param mixed $offset + * @return bool + */ + public function offsetExists($offset) + { + return $this->__isset($offset); + } + + /** + * offsetGet(): defined by ArrayAccess interface. + * + * @see ArrayAccess::offsetGet() + * @param mixed $offset + * @return mixed + */ + public function offsetGet($offset) + { + return $this->__get($offset); + } + + /** + * offsetSet(): defined by ArrayAccess interface. + * + * @see ArrayAccess::offsetSet() + * @param mixed $offset + * @param mixed $value + * @return void + */ + public function offsetSet($offset, $value) + { + $this->__set($offset, $value); + } + + /** + * offsetUnset(): defined by ArrayAccess interface. + * + * @see ArrayAccess::offsetUnset() + * @param mixed $offset + * @return void + */ + public function offsetUnset($offset) + { + $this->__unset($offset); + } + + /** + * Merge another Config with this one. + * + * For duplicate keys, the following will be performed: + * - Nested Configs will be recursively merged. + * - Items in $merge with INTEGER keys will be appended. + * - Items in $merge with STRING keys will overwrite current values. + * + * @param Config $merge + * @return Config + */ + public function merge(Config $merge) + { + /** @var Config $value */ + foreach ($merge as $key => $value) { + if (array_key_exists($key, $this->data)) { + if (is_int($key)) { + $this->data[] = $value; + } elseif ($value instanceof self && $this->data[$key] instanceof self) { + $this->data[$key]->merge($value); + } else { + if ($value instanceof self) { + $this->data[$key] = new static($value->toArray(), $this->allowModifications); + } else { + $this->data[$key] = $value; + } + } + } else { + if ($value instanceof self) { + $this->data[$key] = new static($value->toArray(), $this->allowModifications); + } else { + $this->data[$key] = $value; + } + + $this->count++; + } + } + + return $this; + } + + /** + * Prevent any more modifications being made to this instance. + * + * Useful after merge() has been used to merge multiple Config objects + * into one object which should then not be modified again. + * + * @return void + */ + public function setReadOnly() + { + $this->allowModifications = false; + + /** @var Config $value */ + foreach ($this->data as $value) { + if ($value instanceof self) { + $value->setReadOnly(); + } + } + } + + /** + * Returns whether this Config object is read only or not. + * + * @return bool + */ + public function isReadOnly() + { + return !$this->allowModifications; + } +} diff --git a/library/Zend/Config/Exception/ExceptionInterface.php b/library/Zend/Config/Exception/ExceptionInterface.php new file mode 100755 index 0000000000..d6e0240f10 --- /dev/null +++ b/library/Zend/Config/Exception/ExceptionInterface.php @@ -0,0 +1,14 @@ + 'ini', + 'json' => 'json', + 'xml' => 'xml', + 'yaml' => 'yaml', + ); + + /** + * Register config file extensions for writing + * key is extension, value is writer instance or plugin name + * + * @var array + */ + protected static $writerExtensions = array( + 'php' => 'php', + 'ini' => 'ini', + 'json' => 'json', + 'xml' => 'xml', + 'yaml' => 'yaml', + ); + + /** + * Read a config from a file. + * + * @param string $filename + * @param bool $returnConfigObject + * @param bool $useIncludePath + * @return array|Config + * @throws Exception\InvalidArgumentException + * @throws Exception\RuntimeException + */ + public static function fromFile($filename, $returnConfigObject = false, $useIncludePath = false) + { + $filepath = $filename; + if (!file_exists($filename)) { + if (!$useIncludePath) { + throw new Exception\RuntimeException(sprintf( + 'Filename "%s" cannot be found relative to the working directory', + $filename + )); + } + + $fromIncludePath = stream_resolve_include_path($filename); + if (!$fromIncludePath) { + throw new Exception\RuntimeException(sprintf( + 'Filename "%s" cannot be found relative to the working directory or the include_path ("%s")', + $filename, + get_include_path() + )); + } + $filepath = $fromIncludePath; + } + + $pathinfo = pathinfo($filepath); + + if (!isset($pathinfo['extension'])) { + throw new Exception\RuntimeException(sprintf( + 'Filename "%s" is missing an extension and cannot be auto-detected', + $filename + )); + } + + $extension = strtolower($pathinfo['extension']); + + if ($extension === 'php') { + if (!is_file($filepath) || !is_readable($filepath)) { + throw new Exception\RuntimeException(sprintf( + "File '%s' doesn't exist or not readable", + $filename + )); + } + + $config = include $filepath; + } elseif (isset(static::$extensions[$extension])) { + $reader = static::$extensions[$extension]; + if (!$reader instanceof Reader\ReaderInterface) { + $reader = static::getReaderPluginManager()->get($reader); + static::$extensions[$extension] = $reader; + } + + /** @var Reader\ReaderInterface $reader */ + $config = $reader->fromFile($filepath); + } else { + throw new Exception\RuntimeException(sprintf( + 'Unsupported config file extension: .%s', + $pathinfo['extension'] + )); + } + + return ($returnConfigObject) ? new Config($config) : $config; + } + + /** + * Read configuration from multiple files and merge them. + * + * @param array $files + * @param bool $returnConfigObject + * @param bool $useIncludePath + * @return array|Config + */ + public static function fromFiles(array $files, $returnConfigObject = false, $useIncludePath = false) + { + $config = array(); + + foreach ($files as $file) { + $config = ArrayUtils::merge($config, static::fromFile($file, false, $useIncludePath)); + } + + return ($returnConfigObject) ? new Config($config) : $config; + } + + /** + * Writes a config to a file + * + * @param string $filename + * @param array|Config $config + * @return bool TRUE on success | FALSE on failure + * @throws Exception\RuntimeException + * @throws Exception\InvalidArgumentException + */ + public static function toFile($filename, $config) + { + if ( + (is_object($config) && !($config instanceof Config)) || + (!is_object($config) && !is_array($config)) + ) { + throw new Exception\InvalidArgumentException( + __METHOD__." \$config should be an array or instance of Zend\\Config\\Config" + ); + } + + $extension = substr(strrchr($filename, '.'), 1); + $directory = dirname($filename); + + if (!is_dir($directory)) { + throw new Exception\RuntimeException( + "Directory '{$directory}' does not exists!" + ); + } + + if (!is_writable($directory)) { + throw new Exception\RuntimeException( + "Cannot write in directory '{$directory}'" + ); + } + + if (!isset(static::$writerExtensions[$extension])) { + throw new Exception\RuntimeException( + "Unsupported config file extension: '.{$extension}' for writing." + ); + } + + $writer = static::$writerExtensions[$extension]; + if (($writer instanceof Writer\AbstractWriter) === false) { + $writer = self::getWriterPluginManager()->get($writer); + static::$writerExtensions[$extension] = $writer; + } + + if (is_object($config)) { + $config = $config->toArray(); + } + + $content = $writer->processConfig($config); + + return (bool) (file_put_contents($filename, $content) !== false); + } + + /** + * Set reader plugin manager + * + * @param ReaderPluginManager $readers + * @return void + */ + public static function setReaderPluginManager(ReaderPluginManager $readers) + { + static::$readers = $readers; + } + + /** + * Get the reader plugin manager + * + * @return ReaderPluginManager + */ + public static function getReaderPluginManager() + { + if (static::$readers === null) { + static::$readers = new ReaderPluginManager(); + } + return static::$readers; + } + + /** + * Set writer plugin manager + * + * @param WriterPluginManager $writers + * @return void + */ + public static function setWriterPluginManager(WriterPluginManager $writers) + { + static::$writers = $writers; + } + + /** + * Get the writer plugin manager + * + * @return WriterPluginManager + */ + public static function getWriterPluginManager() + { + if (static::$writers === null) { + static::$writers = new WriterPluginManager(); + } + + return static::$writers; + } + + /** + * Set config reader for file extension + * + * @param string $extension + * @param string|Reader\ReaderInterface $reader + * @throws Exception\InvalidArgumentException + * @return void + */ + public static function registerReader($extension, $reader) + { + $extension = strtolower($extension); + + if (!is_string($reader) && !$reader instanceof Reader\ReaderInterface) { + throw new Exception\InvalidArgumentException(sprintf( + 'Reader should be plugin name, class name or ' . + 'instance of %s\Reader\ReaderInterface; received "%s"', + __NAMESPACE__, + (is_object($reader) ? get_class($reader) : gettype($reader)) + )); + } + + static::$extensions[$extension] = $reader; + } + + /** + * Set config writer for file extension + * + * @param string $extension + * @param string|Writer\AbstractWriter $writer + * @throws Exception\InvalidArgumentException + * @return void + */ + public static function registerWriter($extension, $writer) + { + $extension = strtolower($extension); + + if (!is_string($writer) && !$writer instanceof Writer\AbstractWriter) { + throw new Exception\InvalidArgumentException(sprintf( + 'Writer should be plugin name, class name or ' . + 'instance of %s\Writer\AbstractWriter; received "%s"', + __NAMESPACE__, + (is_object($writer) ? get_class($writer) : gettype($writer)) + )); + } + + static::$writerExtensions[$extension] = $writer; + } +} diff --git a/library/Zend/Config/Processor/Constant.php b/library/Zend/Config/Processor/Constant.php new file mode 100755 index 0000000000..28f76b9a75 --- /dev/null +++ b/library/Zend/Config/Processor/Constant.php @@ -0,0 +1,83 @@ +setUserOnly($userOnly); + $this->setPrefix($prefix); + $this->setSuffix($suffix); + + $this->loadConstants(); + } + + /** + * @return bool + */ + public function getUserOnly() + { + return $this->userOnly; + } + + /** + * Should we use only user-defined constants? + * + * @param bool $userOnly + * @return Constant + */ + public function setUserOnly($userOnly) + { + $this->userOnly = (bool) $userOnly; + return $this; + } + + /** + * Load all currently defined constants into parser. + * + * @return void + */ + public function loadConstants() + { + if ($this->userOnly) { + $constants = get_defined_constants(true); + $constants = isset($constants['user']) ? $constants['user'] : array(); + $this->setTokens($constants); + } else { + $this->setTokens(get_defined_constants()); + } + } + + /** + * Get current token registry. + * @return array + */ + public function getTokens() + { + return $this->tokens; + } +} diff --git a/library/Zend/Config/Processor/Filter.php b/library/Zend/Config/Processor/Filter.php new file mode 100755 index 0000000000..99909f289a --- /dev/null +++ b/library/Zend/Config/Processor/Filter.php @@ -0,0 +1,88 @@ +setFilter($filter); + } + + /** + * @param ZendFilter $filter + * @return Filter + */ + public function setFilter(ZendFilter $filter) + { + $this->filter = $filter; + return $this; + } + + /** + * @return ZendFilter + */ + public function getFilter() + { + return $this->filter; + } + + /** + * Process + * + * @param Config $config + * @return Config + * @throws Exception\InvalidArgumentException + */ + public function process(Config $config) + { + if ($config->isReadOnly()) { + throw new Exception\InvalidArgumentException('Cannot process config because it is read-only'); + } + + /** + * Walk through config and replace values + */ + foreach ($config as $key => $val) { + if ($val instanceof Config) { + $this->process($val); + } else { + $config->$key = $this->filter->filter($val); + } + } + + return $config; + } + + /** + * Process a single value + * + * @param mixed $value + * @return mixed + */ + public function processValue($value) + { + return $this->filter->filter($value); + } +} diff --git a/library/Zend/Config/Processor/ProcessorInterface.php b/library/Zend/Config/Processor/ProcessorInterface.php new file mode 100755 index 0000000000..6aa28e91c0 --- /dev/null +++ b/library/Zend/Config/Processor/ProcessorInterface.php @@ -0,0 +1,31 @@ +isReadOnly()) { + throw new Exception\InvalidArgumentException('Cannot process config because it is read-only'); + } + + foreach ($this as $parser) { + /** @var $parser ProcessorInterface */ + $parser->process($config); + } + } + + /** + * Process a single value + * + * @param mixed $value + * @return mixed + */ + public function processValue($value) + { + foreach ($this as $parser) { + /** @var $parser ProcessorInterface */ + $value = $parser->processValue($value); + } + + return $value; + } +} diff --git a/library/Zend/Config/Processor/Token.php b/library/Zend/Config/Processor/Token.php new file mode 100755 index 0000000000..2af2e1b3f4 --- /dev/null +++ b/library/Zend/Config/Processor/Token.php @@ -0,0 +1,274 @@ + value + * to replace it with + * @param string $prefix + * @param string $suffix + * @internal param array $options + * @return Token + */ + public function __construct($tokens = array(), $prefix = '', $suffix = '') + { + $this->setTokens($tokens); + $this->setPrefix($prefix); + $this->setSuffix($suffix); + } + + /** + * @param string $prefix + * @return Token + */ + public function setPrefix($prefix) + { + // reset map + $this->map = null; + $this->prefix = $prefix; + return $this; + } + + /** + * @return string + */ + public function getPrefix() + { + return $this->prefix; + } + + /** + * @param string $suffix + * @return Token + */ + public function setSuffix($suffix) + { + // reset map + $this->map = null; + $this->suffix = $suffix; + + return $this; + } + + /** + * @return string + */ + public function getSuffix() + { + return $this->suffix; + } + + /** + * Set token registry. + * + * @param array|Config|Traversable $tokens Associative array of TOKEN => value + * to replace it with + * @return Token + * @throws Exception\InvalidArgumentException + */ + public function setTokens($tokens) + { + if (is_array($tokens)) { + $this->tokens = $tokens; + } elseif ($tokens instanceof Config) { + $this->tokens = $tokens->toArray(); + } elseif ($tokens instanceof Traversable) { + $this->tokens = array(); + foreach ($tokens as $key => $val) { + $this->tokens[$key] = $val; + } + } else { + throw new Exception\InvalidArgumentException('Cannot use ' . gettype($tokens) . ' as token registry.'); + } + + // reset map + $this->map = null; + + return $this; + } + + /** + * Get current token registry. + * + * @return array + */ + public function getTokens() + { + return $this->tokens; + } + + /** + * Add new token. + * + * @param string $token + * @param mixed $value + * @return Token + * @throws Exception\InvalidArgumentException + */ + public function addToken($token, $value) + { + if (!is_scalar($token)) { + throw new Exception\InvalidArgumentException('Cannot use ' . gettype($token) . ' as token name.'); + } + $this->tokens[$token] = $value; + + // reset map + $this->map = null; + + return $this; + } + + /** + * Add new token. + * + * @param string $token + * @param mixed $value + * @return Token + */ + public function setToken($token, $value) + { + return $this->addToken($token, $value); + } + + /** + * Build replacement map + * + * @return array + */ + protected function buildMap() + { + if (null === $this->map) { + if (!$this->suffix && !$this->prefix) { + $this->map = $this->tokens; + } else { + $this->map = array(); + + foreach ($this->tokens as $token => $value) { + $this->map[$this->prefix . $token . $this->suffix] = $value; + } + } + + foreach (array_keys($this->map) as $key) { + if (empty($key)) { + unset($this->map[$key]); + } + } + } + + return $this->map; + } + + /** + * Process + * + * @param Config $config + * @return Config + * @throws Exception\InvalidArgumentException + */ + public function process(Config $config) + { + return $this->doProcess($config, $this->buildMap()); + } + + /** + * Process a single value + * + * @param $value + * @return mixed + */ + public function processValue($value) + { + return $this->doProcess($value, $this->buildMap()); + } + + /** + * Applies replacement map to the given value by modifying the value itself + * + * @param mixed $value + * @param array $replacements + * + * @return mixed + * + * @throws Exception\InvalidArgumentException if the provided value is a read-only {@see Config} + */ + private function doProcess($value, array $replacements) + { + if ($value instanceof Config) { + if ($value->isReadOnly()) { + throw new Exception\InvalidArgumentException('Cannot process config because it is read-only'); + } + + foreach ($value as $key => $val) { + $value->$key = $this->doProcess($val, $replacements); + } + + return $value; + } + + if ($value instanceof Traversable || is_array($value)) { + foreach ($value as & $val) { + $val = $this->doProcess($val, $replacements); + } + + return $value; + } + + if (!is_string($value) && (is_bool($value) || is_numeric($value))) { + $stringVal = (string) $value; + $changedVal = strtr($value, $this->map); + + // replace the value only if a string replacement occurred + if ($changedVal !== $stringVal) { + return $changedVal; + } + + return $value; + } + + return strtr((string) $value, $this->map); + } +} diff --git a/library/Zend/Config/Processor/Translator.php b/library/Zend/Config/Processor/Translator.php new file mode 100755 index 0000000000..48c0985de1 --- /dev/null +++ b/library/Zend/Config/Processor/Translator.php @@ -0,0 +1,139 @@ +setTranslator($translator); + $this->setTextDomain($textDomain); + $this->setLocale($locale); + } + + /** + * @param ZendTranslator $translator + * @return Translator + */ + public function setTranslator(ZendTranslator $translator) + { + $this->translator = $translator; + return $this; + } + + /** + * @return ZendTranslator + */ + public function getTranslator() + { + return $this->translator; + } + + /** + * @param string|null $locale + * @return Translator + */ + public function setLocale($locale) + { + $this->locale = $locale; + return $this; + } + + /** + * @return string|null + */ + public function getLocale() + { + return $this->locale; + } + + /** + * @param string $textDomain + * @return Translator + */ + public function setTextDomain($textDomain) + { + $this->textDomain = $textDomain; + return $this; + } + + /** + * @return string + */ + public function getTextDomain() + { + return $this->textDomain; + } + + /** + * Process + * + * @param Config $config + * @return Config + * @throws Exception\InvalidArgumentException + */ + public function process(Config $config) + { + if ($config->isReadOnly()) { + throw new Exception\InvalidArgumentException('Cannot process config because it is read-only'); + } + + /** + * Walk through config and replace values + */ + foreach ($config as $key => $val) { + if ($val instanceof Config) { + $this->process($val); + } else { + $config->{$key} = $this->translator->translate($val, $this->textDomain, $this->locale); + } + } + + return $config; + } + + /** + * Process a single value + * + * @param $value + * @return string + */ + public function processValue($value) + { + return $this->translator->translate($value, $this->textDomain, $this->locale); + } +} diff --git a/library/Zend/Config/README.md b/library/Zend/Config/README.md new file mode 100755 index 0000000000..650ba568d8 --- /dev/null +++ b/library/Zend/Config/README.md @@ -0,0 +1,15 @@ +Config Component from ZF2 +========================= + +This is the Config component for ZF2. + +- File issues at https://github.com/zendframework/zf2/issues +- Create pull requests against https://github.com/zendframework/zf2 +- Documentation is at http://framework.zend.com/docs + +LICENSE +------- + +The files in this archive are released under the [Zend Framework +license](http://framework.zend.com/license), which is a 3-clause BSD license. + diff --git a/library/Zend/Config/Reader/Ini.php b/library/Zend/Config/Reader/Ini.php new file mode 100755 index 0000000000..343668549c --- /dev/null +++ b/library/Zend/Config/Reader/Ini.php @@ -0,0 +1,225 @@ +nestSeparator = $separator; + return $this; + } + + /** + * Get nest separator. + * + * @return string + */ + public function getNestSeparator() + { + return $this->nestSeparator; + } + + /** + * fromFile(): defined by Reader interface. + * + * @see ReaderInterface::fromFile() + * @param string $filename + * @return array + * @throws Exception\RuntimeException + */ + public function fromFile($filename) + { + if (!is_file($filename) || !is_readable($filename)) { + throw new Exception\RuntimeException(sprintf( + "File '%s' doesn't exist or not readable", + $filename + )); + } + + $this->directory = dirname($filename); + + set_error_handler( + function ($error, $message = '', $file = '', $line = 0) use ($filename) { + throw new Exception\RuntimeException( + sprintf('Error reading INI file "%s": %s', $filename, $message), + $error + ); + }, + E_WARNING + ); + $ini = parse_ini_file($filename, true); + restore_error_handler(); + + return $this->process($ini); + } + + /** + * fromString(): defined by Reader interface. + * + * @param string $string + * @return array|bool + * @throws Exception\RuntimeException + */ + public function fromString($string) + { + if (empty($string)) { + return array(); + } + $this->directory = null; + + set_error_handler( + function ($error, $message = '', $file = '', $line = 0) { + throw new Exception\RuntimeException( + sprintf('Error reading INI string: %s', $message), + $error + ); + }, + E_WARNING + ); + $ini = parse_ini_string($string, true); + restore_error_handler(); + + return $this->process($ini); + } + + /** + * Process data from the parsed ini file. + * + * @param array $data + * @return array + */ + protected function process(array $data) + { + $config = array(); + + foreach ($data as $section => $value) { + if (is_array($value)) { + if (strpos($section, $this->nestSeparator) !== false) { + $sections = explode($this->nestSeparator, $section); + $config = array_merge_recursive($config, $this->buildNestedSection($sections, $value)); + } else { + $config[$section] = $this->processSection($value); + } + } else { + $this->processKey($section, $value, $config); + } + } + + return $config; + } + + /** + * Process a nested section + * + * @param array $sections + * @param mixed $value + * @return array + */ + private function buildNestedSection($sections, $value) + { + if (count($sections) == 0) { + return $this->processSection($value); + } + + $nestedSection = array(); + + $first = array_shift($sections); + $nestedSection[$first] = $this->buildNestedSection($sections, $value); + + return $nestedSection; + } + + /** + * Process a section. + * + * @param array $section + * @return array + */ + protected function processSection(array $section) + { + $config = array(); + + foreach ($section as $key => $value) { + $this->processKey($key, $value, $config); + } + + return $config; + } + + /** + * Process a key. + * + * @param string $key + * @param string $value + * @param array $config + * @return array + * @throws Exception\RuntimeException + */ + protected function processKey($key, $value, array &$config) + { + if (strpos($key, $this->nestSeparator) !== false) { + $pieces = explode($this->nestSeparator, $key, 2); + + if (!strlen($pieces[0]) || !strlen($pieces[1])) { + throw new Exception\RuntimeException(sprintf('Invalid key "%s"', $key)); + } elseif (!isset($config[$pieces[0]])) { + if ($pieces[0] === '0' && !empty($config)) { + $config = array($pieces[0] => $config); + } else { + $config[$pieces[0]] = array(); + } + } elseif (!is_array($config[$pieces[0]])) { + throw new Exception\RuntimeException( + sprintf('Cannot create sub-key for "%s", as key already exists', $pieces[0]) + ); + } + + $this->processKey($pieces[1], $value, $config[$pieces[0]]); + } else { + if ($key === '@include') { + if ($this->directory === null) { + throw new Exception\RuntimeException('Cannot process @include statement for a string config'); + } + + $reader = clone $this; + $include = $reader->fromFile($this->directory . '/' . $value); + $config = array_replace_recursive($config, $include); + } else { + $config[$key] = $value; + } + } + } +} diff --git a/library/Zend/Config/Reader/JavaProperties.php b/library/Zend/Config/Reader/JavaProperties.php new file mode 100755 index 0000000000..965bd3c802 --- /dev/null +++ b/library/Zend/Config/Reader/JavaProperties.php @@ -0,0 +1,138 @@ +directory = dirname($filename); + + $config = $this->parse(file_get_contents($filename)); + + return $this->process($config); + } + + /** + * fromString(): defined by Reader interface. + * + * @see ReaderInterface::fromString() + * @param string $string + * @return array + * @throws Exception\RuntimeException if an @include key is found + */ + public function fromString($string) + { + if (empty($string)) { + return array(); + } + + $this->directory = null; + + $config = $this->parse($string); + + return $this->process($config); + } + + /** + * Process the array for @include + * + * @param array $data + * @return array + * @throws Exception\RuntimeException if an @include key is found + */ + protected function process(array $data) + { + foreach ($data as $key => $value) { + if (trim($key) === '@include') { + if ($this->directory === null) { + throw new Exception\RuntimeException('Cannot process @include statement for a string'); + } + $reader = clone $this; + unset($data[$key]); + $data = array_replace_recursive($data, $reader->fromFile($this->directory . '/' . $value)); + } + } + return $data; + } + + /** + * Parse Java-style properties string + * + * @todo Support use of the equals sign "key=value" as key-value delimiter + * @todo Ignore whitespace that precedes text past the first line of multiline values + * + * @param string $string + * @return array + */ + protected function parse($string) + { + $result = array(); + $lines = explode("\n", $string); + $key = ""; + $isWaitingOtherLine = false; + foreach ($lines as $i => $line) { + // Ignore empty lines and commented lines + if (empty($line) + || (!$isWaitingOtherLine && strpos($line, "#") === 0) + || (!$isWaitingOtherLine && strpos($line, "!") === 0)) { + continue; + } + + // Add a new key-value pair or append value to a previous pair + if (!$isWaitingOtherLine) { + $key = substr($line, 0, strpos($line, ':')); + $value = substr($line, strpos($line, ':') + 1, strlen($line)); + } else { + $value .= $line; + } + + // Check if ends with single '\' (indicating another line is expected) + if (strrpos($value, "\\") === strlen($value) - strlen("\\")) { + $value = substr($value, 0, strlen($value) - 1); + $isWaitingOtherLine = true; + } else { + $isWaitingOtherLine = false; + } + + $result[$key] = stripslashes($value); + unset($lines[$i]); + } + + return $result; + } +} diff --git a/library/Zend/Config/Reader/Json.php b/library/Zend/Config/Reader/Json.php new file mode 100755 index 0000000000..407e2aafa5 --- /dev/null +++ b/library/Zend/Config/Reader/Json.php @@ -0,0 +1,105 @@ +directory = dirname($filename); + + try { + $config = JsonFormat::decode(file_get_contents($filename), JsonFormat::TYPE_ARRAY); + } catch (JsonException\RuntimeException $e) { + throw new Exception\RuntimeException($e->getMessage()); + } + + return $this->process($config); + } + + /** + * fromString(): defined by Reader interface. + * + * @see ReaderInterface::fromString() + * @param string $string + * @return array|bool + * @throws Exception\RuntimeException + */ + public function fromString($string) + { + if (empty($string)) { + return array(); + } + + $this->directory = null; + + try { + $config = JsonFormat::decode($string, JsonFormat::TYPE_ARRAY); + } catch (JsonException\RuntimeException $e) { + throw new Exception\RuntimeException($e->getMessage()); + } + + return $this->process($config); + } + + /** + * Process the array for @include + * + * @param array $data + * @return array + * @throws Exception\RuntimeException + */ + protected function process(array $data) + { + foreach ($data as $key => $value) { + if (is_array($value)) { + $data[$key] = $this->process($value); + } + if (trim($key) === '@include') { + if ($this->directory === null) { + throw new Exception\RuntimeException('Cannot process @include statement for a JSON string'); + } + $reader = clone $this; + unset($data[$key]); + $data = array_replace_recursive($data, $reader->fromFile($this->directory . '/' . $value)); + } + } + return $data; + } +} diff --git a/library/Zend/Config/Reader/ReaderInterface.php b/library/Zend/Config/Reader/ReaderInterface.php new file mode 100755 index 0000000000..0393fe0528 --- /dev/null +++ b/library/Zend/Config/Reader/ReaderInterface.php @@ -0,0 +1,29 @@ +reader = new XMLReader(); + $this->reader->open($filename, null, LIBXML_XINCLUDE); + + $this->directory = dirname($filename); + + set_error_handler( + function ($error, $message = '', $file = '', $line = 0) use ($filename) { + throw new Exception\RuntimeException( + sprintf('Error reading XML file "%s": %s', $filename, $message), + $error + ); + }, + E_WARNING + ); + $return = $this->process(); + restore_error_handler(); + + return $return; + } + + /** + * fromString(): defined by Reader interface. + * + * @see ReaderInterface::fromString() + * @param string $string + * @return array|bool + * @throws Exception\RuntimeException + */ + public function fromString($string) + { + if (empty($string)) { + return array(); + } + $this->reader = new XMLReader(); + + $this->reader->xml($string, null, LIBXML_XINCLUDE); + + $this->directory = null; + + set_error_handler( + function ($error, $message = '', $file = '', $line = 0) { + throw new Exception\RuntimeException( + sprintf('Error reading XML string: %s', $message), + $error + ); + }, + E_WARNING + ); + $return = $this->process(); + restore_error_handler(); + + return $return; + } + + /** + * Process data from the created XMLReader. + * + * @return array + */ + protected function process() + { + return $this->processNextElement(); + } + + /** + * Process the next inner element. + * + * @return mixed + */ + protected function processNextElement() + { + $children = array(); + $text = ''; + + while ($this->reader->read()) { + if ($this->reader->nodeType === XMLReader::ELEMENT) { + if ($this->reader->depth === 0) { + return $this->processNextElement(); + } + + $attributes = $this->getAttributes(); + $name = $this->reader->name; + + if ($this->reader->isEmptyElement) { + $child = array(); + } else { + $child = $this->processNextElement(); + } + + if ($attributes) { + if (is_string($child)) { + $child = array('_' => $child); + } + + if (! is_array($child) ) { + $child = array(); + } + + $child = array_merge($child, $attributes); + } + + if (isset($children[$name])) { + if (!is_array($children[$name]) || !array_key_exists(0, $children[$name])) { + $children[$name] = array($children[$name]); + } + + $children[$name][] = $child; + } else { + $children[$name] = $child; + } + } elseif ($this->reader->nodeType === XMLReader::END_ELEMENT) { + break; + } elseif (in_array($this->reader->nodeType, $this->textNodes)) { + $text .= $this->reader->value; + } + } + + return $children ?: $text; + } + + /** + * Get all attributes on the current node. + * + * @return array + */ + protected function getAttributes() + { + $attributes = array(); + + if ($this->reader->hasAttributes) { + while ($this->reader->moveToNextAttribute()) { + $attributes[$this->reader->localName] = $this->reader->value; + } + + $this->reader->moveToElement(); + } + + return $attributes; + } +} diff --git a/library/Zend/Config/Reader/Yaml.php b/library/Zend/Config/Reader/Yaml.php new file mode 100755 index 0000000000..709b8b8a69 --- /dev/null +++ b/library/Zend/Config/Reader/Yaml.php @@ -0,0 +1,159 @@ +setYamlDecoder($yamlDecoder); + } else { + if (function_exists('yaml_parse')) { + $this->setYamlDecoder('yaml_parse'); + } + } + } + + /** + * Set callback for decoding YAML + * + * @param string|callable $yamlDecoder the decoder to set + * @return Yaml + * @throws Exception\RuntimeException + */ + public function setYamlDecoder($yamlDecoder) + { + if (!is_callable($yamlDecoder)) { + throw new Exception\RuntimeException( + 'Invalid parameter to setYamlDecoder() - must be callable' + ); + } + $this->yamlDecoder = $yamlDecoder; + return $this; + } + + /** + * Get callback for decoding YAML + * + * @return callable + */ + public function getYamlDecoder() + { + return $this->yamlDecoder; + } + + /** + * fromFile(): defined by Reader interface. + * + * @see ReaderInterface::fromFile() + * @param string $filename + * @return array + * @throws Exception\RuntimeException + */ + public function fromFile($filename) + { + if (!is_file($filename) || !is_readable($filename)) { + throw new Exception\RuntimeException(sprintf( + "File '%s' doesn't exist or not readable", + $filename + )); + } + + if (null === $this->getYamlDecoder()) { + throw new Exception\RuntimeException("You didn't specify a Yaml callback decoder"); + } + + $this->directory = dirname($filename); + + $config = call_user_func($this->getYamlDecoder(), file_get_contents($filename)); + if (null === $config) { + throw new Exception\RuntimeException("Error parsing YAML data"); + } + + return $this->process($config); + } + + /** + * fromString(): defined by Reader interface. + * + * @see ReaderInterface::fromString() + * @param string $string + * @return array|bool + * @throws Exception\RuntimeException + */ + public function fromString($string) + { + if (null === $this->getYamlDecoder()) { + throw new Exception\RuntimeException("You didn't specify a Yaml callback decoder"); + } + if (empty($string)) { + return array(); + } + + $this->directory = null; + + $config = call_user_func($this->getYamlDecoder(), $string); + if (null === $config) { + throw new Exception\RuntimeException("Error parsing YAML data"); + } + + return $this->process($config); + } + + /** + * Process the array for @include + * + * @param array $data + * @return array + * @throws Exception\RuntimeException + */ + protected function process(array $data) + { + foreach ($data as $key => $value) { + if (is_array($value)) { + $data[$key] = $this->process($value); + } + if (trim($key) === '@include') { + if ($this->directory === null) { + throw new Exception\RuntimeException('Cannot process @include statement for a json string'); + } + $reader = clone $this; + unset($data[$key]); + $data = array_replace_recursive($data, $reader->fromFile($this->directory . '/' . $value)); + } + } + return $data; + } +} diff --git a/library/Zend/Config/ReaderPluginManager.php b/library/Zend/Config/ReaderPluginManager.php new file mode 100755 index 0000000000..3e74a5640c --- /dev/null +++ b/library/Zend/Config/ReaderPluginManager.php @@ -0,0 +1,49 @@ + 'Zend\Config\Reader\Ini', + 'json' => 'Zend\Config\Reader\Json', + 'xml' => 'Zend\Config\Reader\Xml', + 'yaml' => 'Zend\Config\Reader\Yaml', + ); + + /** + * Validate the plugin + * Checks that the reader loaded is an instance of Reader\ReaderInterface. + * + * @param Reader\ReaderInterface $plugin + * @return void + * @throws Exception\InvalidArgumentException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof Reader\ReaderInterface) { + // we're okay + return; + } + + throw new Exception\InvalidArgumentException(sprintf( + 'Plugin of type %s is invalid; must implement %s\Reader\ReaderInterface', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/library/Zend/Config/Writer/AbstractWriter.php b/library/Zend/Config/Writer/AbstractWriter.php new file mode 100755 index 0000000000..a02111f7db --- /dev/null +++ b/library/Zend/Config/Writer/AbstractWriter.php @@ -0,0 +1,84 @@ +toString($config), $flags); + } catch (\Exception $e) { + restore_error_handler(); + throw $e; + } + + restore_error_handler(); + } + + /** + * toString(): defined by Writer interface. + * + * @see WriterInterface::toString() + * @param mixed $config + * @return string + * @throws Exception\InvalidArgumentException + */ + public function toString($config) + { + if ($config instanceof Traversable) { + $config = ArrayUtils::iteratorToArray($config); + } elseif (!is_array($config)) { + throw new Exception\InvalidArgumentException(__METHOD__ . ' expects an array or Traversable config'); + } + + return $this->processConfig($config); + } + + /** + * @param array $config + * @return string + */ + abstract protected function processConfig(array $config); +} diff --git a/library/Zend/Config/Writer/Ini.php b/library/Zend/Config/Writer/Ini.php new file mode 100755 index 0000000000..9244f73e62 --- /dev/null +++ b/library/Zend/Config/Writer/Ini.php @@ -0,0 +1,183 @@ +nestSeparator = $separator; + return $this; + } + + /** + * Get nest separator. + * + * @return string + */ + public function getNestSeparator() + { + return $this->nestSeparator; + } + + /** + * Set if rendering should occur without sections or not. + * + * If set to true, the INI file is rendered without sections completely + * into the global namespace of the INI file. + * + * @param bool $withoutSections + * @return Ini + */ + public function setRenderWithoutSectionsFlags($withoutSections) + { + $this->renderWithoutSections = (bool) $withoutSections; + return $this; + } + + /** + * Return whether the writer should render without sections. + * + * @return bool + */ + public function shouldRenderWithoutSections() + { + return $this->renderWithoutSections; + } + + /** + * processConfig(): defined by AbstractWriter. + * + * @param array $config + * @return string + */ + public function processConfig(array $config) + { + $iniString = ''; + + if ($this->shouldRenderWithoutSections()) { + $iniString .= $this->addBranch($config); + } else { + $config = $this->sortRootElements($config); + + foreach ($config as $sectionName => $data) { + if (!is_array($data)) { + $iniString .= $sectionName + . ' = ' + . $this->prepareValue($data) + . "\n"; + } else { + $iniString .= '[' . $sectionName . ']' . "\n" + . $this->addBranch($data) + . "\n"; + } + } + } + + return $iniString; + } + + /** + * Add a branch to an INI string recursively. + * + * @param array $config + * @param array $parents + * @return string + */ + protected function addBranch(array $config, $parents = array()) + { + $iniString = ''; + + foreach ($config as $key => $value) { + $group = array_merge($parents, array($key)); + + if (is_array($value)) { + $iniString .= $this->addBranch($value, $group); + } else { + $iniString .= implode($this->nestSeparator, $group) + . ' = ' + . $this->prepareValue($value) + . "\n"; + } + } + + return $iniString; + } + + /** + * Prepare a value for INI. + * + * @param mixed $value + * @return string + * @throws Exception\RuntimeException + */ + protected function prepareValue($value) + { + if (is_int($value) || is_float($value)) { + return $value; + } elseif (is_bool($value)) { + return ($value ? 'true' : 'false'); + } elseif (false === strpos($value, '"')) { + return '"' . $value . '"'; + } else { + throw new Exception\RuntimeException('Value can not contain double quotes'); + } + } + + /** + * Root elements that are not assigned to any section needs to be on the + * top of config. + * + * @param array $config + * @return array + */ + protected function sortRootElements(array $config) + { + $sections = array(); + + // Remove sections from config array. + foreach ($config as $key => $value) { + if (is_array($value)) { + $sections[$key] = $value; + unset($config[$key]); + } + } + + // Read sections to the end. + foreach ($sections as $key => $value) { + $config[$key] = $value; + } + + return $config; + } +} diff --git a/library/Zend/Config/Writer/Json.php b/library/Zend/Config/Writer/Json.php new file mode 100755 index 0000000000..04548495b7 --- /dev/null +++ b/library/Zend/Config/Writer/Json.php @@ -0,0 +1,26 @@ + $this->useBracketArraySyntax ? '[' : 'array(', + 'close' => $this->useBracketArraySyntax ? ']' : ')' + ); + + return "processIndented($config, $arraySyntax) . + $arraySyntax['close'] . ";\n"; + } + + /** + * Sets whether or not to use the PHP 5.4+ "[]" array syntax. + * + * @param bool $value + * @return self + */ + public function setUseBracketArraySyntax($value) + { + $this->useBracketArraySyntax = $value; + return $this; + } + + /** + * toFile(): defined by Writer interface. + * + * @see WriterInterface::toFile() + * @param string $filename + * @param mixed $config + * @param bool $exclusiveLock + * @return void + * @throws Exception\InvalidArgumentException + * @throws Exception\RuntimeException + */ + public function toFile($filename, $config, $exclusiveLock = true) + { + if (empty($filename)) { + throw new Exception\InvalidArgumentException('No file name specified'); + } + + $flags = 0; + if ($exclusiveLock) { + $flags |= LOCK_EX; + } + + set_error_handler( + function ($error, $message = '', $file = '', $line = 0) use ($filename) { + throw new Exception\RuntimeException( + sprintf('Error writing to "%s": %s', $filename, $message), + $error + ); + }, + E_WARNING + ); + + try { + // for Windows, paths are escaped. + $dirname = str_replace('\\', '\\\\', dirname($filename)); + + $string = $this->toString($config); + $string = str_replace("'" . $dirname, "__DIR__ . '", $string); + + file_put_contents($filename, $string, $flags); + } catch (\Exception $e) { + restore_error_handler(); + throw $e; + } + + restore_error_handler(); + } + + /** + * Recursively processes a PHP config array structure into a readable format. + * + * @param array $config + * @param array $arraySyntax + * @param int $indentLevel + * @return string + */ + protected function processIndented(array $config, array $arraySyntax, &$indentLevel = 1) + { + $arrayString = ""; + + foreach ($config as $key => $value) { + $arrayString .= str_repeat(self::INDENT_STRING, $indentLevel); + $arrayString .= (is_int($key) ? $key : "'" . addslashes($key) . "'") . ' => '; + + if (is_array($value)) { + if ($value === array()) { + $arrayString .= $arraySyntax['open'] . $arraySyntax['close'] . ",\n"; + } else { + $indentLevel++; + $arrayString .= $arraySyntax['open'] . "\n" + . $this->processIndented($value, $arraySyntax, $indentLevel) + . str_repeat(self::INDENT_STRING, --$indentLevel) . $arraySyntax['close'] . ",\n"; + } + } elseif (is_object($value) || is_string($value)) { + $arrayString .= var_export($value, true) . ",\n"; + } elseif (is_bool($value)) { + $arrayString .= ($value ? 'true' : 'false') . ",\n"; + } elseif ($value === null) { + $arrayString .= "null,\n"; + } else { + $arrayString .= $value . ",\n"; + } + } + + return $arrayString; + } +} diff --git a/library/Zend/Config/Writer/WriterInterface.php b/library/Zend/Config/Writer/WriterInterface.php new file mode 100755 index 0000000000..afbecd104b --- /dev/null +++ b/library/Zend/Config/Writer/WriterInterface.php @@ -0,0 +1,31 @@ +openMemory(); + $writer->setIndent(true); + $writer->setIndentString(str_repeat(' ', 4)); + + $writer->startDocument('1.0', 'UTF-8'); + $writer->startElement('zend-config'); + + foreach ($config as $sectionName => $data) { + if (!is_array($data)) { + $writer->writeElement($sectionName, (string) $data); + } else { + $this->addBranch($sectionName, $data, $writer); + } + } + + $writer->endElement(); + $writer->endDocument(); + + return $writer->outputMemory(); + } + + /** + * Add a branch to an XML object recursively. + * + * @param string $branchName + * @param array $config + * @param XMLWriter $writer + * @return void + * @throws Exception\RuntimeException + */ + protected function addBranch($branchName, array $config, XMLWriter $writer) + { + $branchType = null; + + foreach ($config as $key => $value) { + if ($branchType === null) { + if (is_numeric($key)) { + $branchType = 'numeric'; + } else { + $writer->startElement($branchName); + $branchType = 'string'; + } + } elseif ($branchType !== (is_numeric($key) ? 'numeric' : 'string')) { + throw new Exception\RuntimeException('Mixing of string and numeric keys is not allowed'); + } + + if ($branchType === 'numeric') { + if (is_array($value)) { + $this->addBranch($value, $value, $writer); + } else { + $writer->writeElement($branchName, (string) $value); + } + } else { + if (is_array($value)) { + $this->addBranch($key, $value, $writer); + } else { + $writer->writeElement($key, (string) $value); + } + } + } + + if ($branchType === 'string') { + $writer->endElement(); + } + } +} diff --git a/library/Zend/Config/Writer/Yaml.php b/library/Zend/Config/Writer/Yaml.php new file mode 100755 index 0000000000..f741ad677f --- /dev/null +++ b/library/Zend/Config/Writer/Yaml.php @@ -0,0 +1,85 @@ +setYamlEncoder($yamlEncoder); + } else { + if (function_exists('yaml_emit')) { + $this->setYamlEncoder('yaml_emit'); + } + } + } + + /** + * Get callback for decoding YAML + * + * @return callable + */ + public function getYamlEncoder() + { + return $this->yamlEncoder; + } + + /** + * Set callback for decoding YAML + * + * @param callable $yamlEncoder the decoder to set + * @return Yaml + * @throws Exception\InvalidArgumentException + */ + public function setYamlEncoder($yamlEncoder) + { + if (!is_callable($yamlEncoder)) { + throw new Exception\InvalidArgumentException('Invalid parameter to setYamlEncoder() - must be callable'); + } + $this->yamlEncoder = $yamlEncoder; + return $this; + } + + /** + * processConfig(): defined by AbstractWriter. + * + * @param array $config + * @return string + * @throws Exception\RuntimeException + */ + public function processConfig(array $config) + { + if (null === $this->getYamlEncoder()) { + throw new Exception\RuntimeException("You didn't specify a Yaml callback encoder"); + } + + $config = call_user_func($this->getYamlEncoder(), $config); + if (null === $config) { + throw new Exception\RuntimeException("Error generating YAML data"); + } + + return $config; + } +} diff --git a/library/Zend/Config/WriterPluginManager.php b/library/Zend/Config/WriterPluginManager.php new file mode 100755 index 0000000000..c70e4415ce --- /dev/null +++ b/library/Zend/Config/WriterPluginManager.php @@ -0,0 +1,36 @@ + 'Zend\Config\Writer\Ini', + 'json' => 'Zend\Config\Writer\Json', + 'php' => 'Zend\Config\Writer\PhpArray', + 'yaml' => 'Zend\Config\Writer\Yaml', + 'xml' => 'Zend\Config\Writer\Xml', + ); + + public function validatePlugin($plugin) + { + if ($plugin instanceof Writer\AbstractWriter) { + return; + } + + $type = is_object($plugin) ? get_class($plugin) : gettype($plugin); + + throw new Exception\InvalidArgumentException( + "Plugin of type {$type} is invalid. Plugin must extend ". __NAMESPACE__ . '\Writer\AbstractWriter' + ); + } +} diff --git a/library/Zend/Config/composer.json b/library/Zend/Config/composer.json new file mode 100755 index 0000000000..6ea7fa11ef --- /dev/null +++ b/library/Zend/Config/composer.json @@ -0,0 +1,38 @@ +{ + "name": "zendframework/zend-config", + "description": "provides a nested object property based user interface for accessing this configuration data within application code", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "config" + ], + "homepage": "https://github.com/zendframework/zf2", + "autoload": { + "psr-0": { + "Zend\\Config\\": "" + } + }, + "target-dir": "Zend/Config", + "require": { + "php": ">=5.3.23", + "zendframework/zend-stdlib": "self.version" + }, + "require-dev": { + "zendframework/zend-filter": "self.version", + "zendframework/zend-i18n": "self.version", + "zendframework/zend-json": "self.version", + "zendframework/zend-servicemanager": "self.version" + }, + "suggest": { + "zendframework/zend-filter": "Zend\\Filter component", + "zendframework/zend-i18n": "Zend\\I18n component", + "zendframework/zend-json": "Zend\\Json to use the Json reader or writer classes", + "zendframework/zend-servicemanager": "Zend\\ServiceManager for use with the Config Factory to retrieve reader and writer instances" + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev", + "dev-develop": "2.4-dev" + } + } +} diff --git a/library/Zend/Console/Adapter/AbstractAdapter.php b/library/Zend/Console/Adapter/AbstractAdapter.php new file mode 100755 index 0000000000..49718034fe --- /dev/null +++ b/library/Zend/Console/Adapter/AbstractAdapter.php @@ -0,0 +1,561 @@ +encodeText($text); + + if ($color !== null || $bgColor !== null) { + echo $this->colorize($text, $color, $bgColor); + } else { + echo $text; + } + } + + /** + * Alias for write() + * + * @param string $text + * @param null|int $color + * @param null|int $bgColor + */ + public function writeText($text, $color = null, $bgColor = null) + { + return $this->write($text, $color, $bgColor); + } + + /** + * Write a single line of text to console and advance cursor to the next line. + * + * @param string $text + * @param null|int $color + * @param null|int $bgColor + */ + public function writeLine($text = "", $color = null, $bgColor = null) + { + $this->write($text . PHP_EOL, $color, $bgColor); + } + + /** + * Write a piece of text at the coordinates of $x and $y + * + * + * @param string $text Text to write + * @param int $x Console X coordinate (column) + * @param int $y Console Y coordinate (row) + * @param null|int $color + * @param null|int $bgColor + */ + public function writeAt($text, $x, $y, $color = null, $bgColor = null) + { + $this->setPos($x, $y); + $this->write($text, $color, $bgColor); + } + + /** + * Write a box at the specified coordinates. + * If X or Y coordinate value is negative, it will be calculated as the distance from far right or bottom edge + * of the console (respectively). + * + * @param int $x1 Top-left corner X coordinate (column) + * @param int $y1 Top-left corner Y coordinate (row) + * @param int $x2 Bottom-right corner X coordinate (column) + * @param int $y2 Bottom-right corner Y coordinate (row) + * @param int $lineStyle (optional) Box border style. + * @param int $fillStyle (optional) Box fill style or a single character to fill it with. + * @param int $color (optional) Foreground color + * @param int $bgColor (optional) Background color + * @param null|int $fillColor (optional) Foreground color of box fill + * @param null|int $fillBgColor (optional) Background color of box fill + * @throws Exception\BadMethodCallException if coordinates are invalid + */ + public function writeBox( + $x1, + $y1, + $x2, + $y2, + $lineStyle = self::LINE_SINGLE, + $fillStyle = self::FILL_NONE, + $color = null, + $bgColor = null, + $fillColor = null, + $fillBgColor = null + ) { + // Sanitize coordinates + $x1 = (int) $x1; + $y1 = (int) $y1; + $x2 = (int) $x2; + $y2 = (int) $y2; + + // Translate negative coordinates + if ($x2 < 0) { + $x2 = $this->getWidth() - $x2; + } + + if ($y2 < 0) { + $y2 = $this->getHeight() - $y2; + } + + // Validate coordinates + if ($x1 < 0 + || $y1 < 0 + || $x2 < $x1 + || $y2 < $y1 + ) { + throw new Exception\BadMethodCallException('Supplied X,Y coordinates are invalid.'); + } + + // Determine charset and dimensions + $charset = $this->getCharset(); + $width = $x2 - $x1 + 1; + $height = $y2 - $y1 + 1; + + if ($width <= 2) { + $lineStyle = static::LINE_NONE; + } + + // Activate line drawing + $this->write($charset::ACTIVATE); + + // Draw horizontal lines + if ($lineStyle !== static::LINE_NONE) { + switch ($lineStyle) { + case static::LINE_SINGLE: + $lineChar = $charset::LINE_SINGLE_EW; + break; + + case static::LINE_DOUBLE: + $lineChar = $charset::LINE_DOUBLE_EW; + break; + + case static::LINE_BLOCK: + default: + $lineChar = $charset::LINE_BLOCK_EW; + break; + } + + $this->setPos($x1 + 1, $y1); + $this->write(str_repeat($lineChar, $width - 2), $color, $bgColor); + $this->setPos($x1 + 1, $y2); + $this->write(str_repeat($lineChar, $width - 2), $color, $bgColor); + } + + // Draw vertical lines and fill + if (is_numeric($fillStyle) + && $fillStyle !== static::FILL_NONE) { + switch ($fillStyle) { + case static::FILL_SHADE_LIGHT: + $fillChar = $charset::SHADE_LIGHT; + break; + case static::FILL_SHADE_MEDIUM: + $fillChar = $charset::SHADE_MEDIUM; + break; + case static::FILL_SHADE_DARK: + $fillChar = $charset::SHADE_DARK; + break; + case static::FILL_BLOCK: + default: + $fillChar = $charset::BLOCK; + break; + } + } elseif ($fillStyle) { + $fillChar = StringUtils::getWrapper()->substr($fillStyle, 0, 1); + } else { + $fillChar = ' '; + } + + if ($lineStyle === static::LINE_NONE) { + for ($y = $y1; $y <= $y2; $y++) { + $this->setPos($x1, $y); + $this->write(str_repeat($fillChar, $width), $fillColor, $fillBgColor); + } + } else { + switch ($lineStyle) { + case static::LINE_DOUBLE: + $lineChar = $charset::LINE_DOUBLE_NS; + break; + case static::LINE_BLOCK: + $lineChar = $charset::LINE_BLOCK_NS; + break; + case static::LINE_SINGLE: + default: + $lineChar = $charset::LINE_SINGLE_NS; + break; + } + + for ($y = $y1 + 1; $y < $y2; $y++) { + $this->setPos($x1, $y); + $this->write($lineChar, $color, $bgColor); + $this->write(str_repeat($fillChar, $width - 2), $fillColor, $fillBgColor); + $this->write($lineChar, $color, $bgColor); + } + } + + // Draw corners + if ($lineStyle !== static::LINE_NONE) { + if ($color !== null) { + $this->setColor($color); + } + if ($bgColor !== null) { + $this->setBgColor($bgColor); + } + if ($lineStyle === static::LINE_SINGLE) { + $this->writeAt($charset::LINE_SINGLE_NW, $x1, $y1); + $this->writeAt($charset::LINE_SINGLE_NE, $x2, $y1); + $this->writeAt($charset::LINE_SINGLE_SE, $x2, $y2); + $this->writeAt($charset::LINE_SINGLE_SW, $x1, $y2); + } elseif ($lineStyle === static::LINE_DOUBLE) { + $this->writeAt($charset::LINE_DOUBLE_NW, $x1, $y1); + $this->writeAt($charset::LINE_DOUBLE_NE, $x2, $y1); + $this->writeAt($charset::LINE_DOUBLE_SE, $x2, $y2); + $this->writeAt($charset::LINE_DOUBLE_SW, $x1, $y2); + } elseif ($lineStyle === static::LINE_BLOCK) { + $this->writeAt($charset::LINE_BLOCK_NW, $x1, $y1); + $this->writeAt($charset::LINE_BLOCK_NE, $x2, $y1); + $this->writeAt($charset::LINE_BLOCK_SE, $x2, $y2); + $this->writeAt($charset::LINE_BLOCK_SW, $x1, $y2); + } + } + + // Deactivate line drawing and reset colors + $this->write($charset::DEACTIVATE); + $this->resetColor(); + } + + /** + * Write a block of text at the given coordinates, matching the supplied width and height. + * In case a line of text does not fit desired width, it will be wrapped to the next line. + * In case the whole text does not fit in desired height, it will be truncated. + * + * @param string $text Text to write + * @param int $width Maximum block width. Negative value means distance from right edge. + * @param int|null $height Maximum block height. Negative value means distance from bottom edge. + * @param int $x Block X coordinate (column) + * @param int $y Block Y coordinate (row) + * @param null|int $color (optional) Text color + * @param null|int $bgColor (optional) Text background color + * @throws Exception\InvalidArgumentException + */ + public function writeTextBlock( + $text, + $width, + $height = null, + $x = 0, + $y = 0, + $color = null, + $bgColor = null + ) { + if ($x < 0 || $y < 0) { + throw new Exception\InvalidArgumentException('Supplied X,Y coordinates are invalid.'); + } + + if ($width < 1) { + throw new Exception\InvalidArgumentException('Invalid width supplied.'); + } + + if (null !== $height && $height < 1) { + throw new Exception\InvalidArgumentException('Invalid height supplied.'); + } + + // ensure the text is not wider than the width + if (strlen($text) <= $width) { + // just write the line at the spec'd position + $this->setPos($x, $y); + $this->write($text, $color, $bgColor); + return; + } + + $text = wordwrap($text, $width, PHP_EOL, true); + + // convert to array of lines + $lines = explode(PHP_EOL, $text); + + // truncate if height was specified + if (null !== $height && count($lines) > $height) { + $lines = array_slice($lines, 0, $height); + } + + // write each line + $curY = $y; + foreach ($lines as $line) { + $this->setPos($x, $curY); + $this->write($line, $color, $bgColor); + $curY++;//next line + } + } + + /** + * Determine and return current console width. + * + * @return int + */ + public function getWidth() + { + return 80; + } + + /** + * Determine and return current console height. + * + * @return int + */ + public function getHeight() + { + return 25; + } + + /** + * Determine and return current console width and height. + * + * @return int[] array($width, $height) + */ + public function getSize() + { + return array( + $this->getWidth(), + $this->getHeight(), + ); + } + + /** + * Check if console is UTF-8 compatible + * + * @return bool + */ + public function isUtf8() + { + return true; + } + + /** + * Set cursor position + * + * @param int $x + * @param int $y + */ + public function setPos($x, $y) + { + } + + /** + * Show console cursor + */ + public function showCursor() + { + } + + /** + * Hide console cursor + */ + public function hideCursor() + { + } + + /** + * Return current console window title. + * + * @return string + */ + public function getTitle() + { + return ''; + } + + /** + * Prepare a string that will be rendered in color. + * + * @param string $string + * @param int $color + * @param null|int $bgColor + * @return string + */ + public function colorize($string, $color = null, $bgColor = null) + { + return $string; + } + + /** + * Change current drawing color. + * + * @param int $color + */ + public function setColor($color) + { + } + + /** + * Change current drawing background color + * + * @param int $color + */ + public function setBgColor($color) + { + } + + /** + * Reset color to console default. + */ + public function resetColor() + { + } + + /** + * Set Console charset to use. + * + * @param Charset\CharsetInterface $charset + */ + public function setCharset(Charset\CharsetInterface $charset) + { + $this->charset = $charset; + } + + /** + * Get charset currently in use by this adapter. + * + * @return Charset\CharsetInterface $charset + */ + public function getCharset() + { + if ($this->charset === null) { + $this->charset = $this->getDefaultCharset(); + } + + return $this->charset; + } + + /** + * @return Charset\Utf8 + */ + public function getDefaultCharset() + { + return new Charset\Utf8; + } + + /** + * Clear console screen + */ + public function clear() + { + echo "\f"; + } + + /** + * Clear line at cursor position + */ + public function clearLine() + { + echo "\r" . str_repeat(" ", $this->getWidth()) . "\r"; + } + + /** + * Clear console screen + */ + public function clearScreen() + { + return $this->clear(); + } + + /** + * Read a single line from the console input + * + * @param int $maxLength Maximum response length + * @return string + */ + public function readLine($maxLength = 2048) + { + $f = fopen('php://stdin', 'r'); + $line = stream_get_line($f, $maxLength, PHP_EOL); + fclose($f); + return rtrim($line, "\n\r"); + } + + /** + * Read a single character from the console input + * + * @param string|null $mask A list of allowed chars + * @return string + */ + public function readChar($mask = null) + { + $f = fopen('php://stdin', 'r'); + do { + $char = fread($f, 1); + } while ("" === $char || ($mask !== null && false === strstr($mask, $char))); + fclose($f); + return $char; + } + + /** + * Encode a text to match console encoding + * + * @param string $text + * @return string the encoding text + */ + public function encodeText($text) + { + if ($this->isUtf8()) { + if (StringUtils::isValidUtf8($text)) { + return $text; + } + + return utf8_encode($text); + } + + if (StringUtils::isValidUtf8($text)) { + return utf8_decode($text); + } + + return $text; + } +} diff --git a/library/Zend/Console/Adapter/AdapterInterface.php b/library/Zend/Console/Adapter/AdapterInterface.php new file mode 100755 index 0000000000..45407b80a1 --- /dev/null +++ b/library/Zend/Console/Adapter/AdapterInterface.php @@ -0,0 +1,264 @@ + array( + Color::NORMAL => '22;39', + Color::RESET => '22;39', + + Color::BLACK => '0;30', + Color::RED => '0;31', + Color::GREEN => '0;32', + Color::YELLOW => '0;33', + Color::BLUE => '0;34', + Color::MAGENTA => '0;35', + Color::CYAN => '0;36', + Color::WHITE => '0;37', + + Color::GRAY => '1;30', + Color::LIGHT_RED => '1;31', + Color::LIGHT_GREEN => '1;32', + Color::LIGHT_YELLOW => '1;33', + Color::LIGHT_BLUE => '1;34', + Color::LIGHT_MAGENTA => '1;35', + Color::LIGHT_CYAN => '1;36', + Color::LIGHT_WHITE => '1;37', + ), + 'bg' => array( + Color::NORMAL => '0;49', + Color::RESET => '0;49', + + Color::BLACK => '40', + Color::RED => '41', + Color::GREEN => '42', + Color::YELLOW => '43', + Color::BLUE => '44', + Color::MAGENTA => '45', + Color::CYAN => '46', + Color::WHITE => '47', + + Color::GRAY => '40', + Color::LIGHT_RED => '41', + Color::LIGHT_GREEN => '42', + Color::LIGHT_YELLOW => '43', + Color::LIGHT_BLUE => '44', + Color::LIGHT_MAGENTA => '45', + Color::LIGHT_CYAN => '46', + Color::LIGHT_WHITE => '47', + ), + ); + + /** + * Last fetched TTY mode + * + * @var string|null + */ + protected $lastTTYMode = null; + + /** + * Write a single line of text to console and advance cursor to the next line. + * + * This override works around a bug in some terminals that cause the background color + * to fill the next line after EOL. To remedy this, we are sending the colored string with + * appropriate color reset sequences before sending EOL character. + * + * @link https://github.com/zendframework/zf2/issues/4167 + * @param string $text + * @param null|int $color + * @param null|int $bgColor + */ + public function writeLine($text = "", $color = null, $bgColor = null) + { + $this->write($text, $color, $bgColor); + $this->write(PHP_EOL); + } + + /** + * Determine and return current console width. + * + * @return int + */ + public function getWidth() + { + static $width; + if ($width > 0) { + return $width; + } + + /** + * Try to read env variable + */ + if (($result = getenv('COLUMNS')) !== false) { + return $width = (int) $result; + } + + /** + * Try to read console size from "tput" command + */ + $result = exec('tput cols', $output, $return); + if (!$return && is_numeric($result)) { + return $width = (int) $result; + } + + return $width = parent::getWidth(); + } + + /** + * Determine and return current console height. + * + * @return false|int + */ + public function getHeight() + { + static $height; + if ($height > 0) { + return $height; + } + + // Try to read env variable + if (($result = getenv('LINES')) !== false) { + return $height = (int) $result; + } + + // Try to read console size from "tput" command + $result = exec('tput lines', $output, $return); + if (!$return && is_numeric($result)) { + return $height = (int) $result; + } + + return $height = parent::getHeight(); + } + + /** + * Run a mode command and store results + * + * @return void + */ + protected function runModeCommand() + { + exec('mode', $output, $return); + if ($return || !count($output)) { + $this->modeResult = ''; + } else { + $this->modeResult = trim(implode('', $output)); + } + } + + /** + * Check if console is UTF-8 compatible + * + * @return bool + */ + public function isUtf8() + { + // Try to retrieve it from LANG env variable + if (($lang = getenv('LANG')) !== false) { + return stristr($lang, 'utf-8') || stristr($lang, 'utf8'); + } + + return false; + } + + /** + * Show console cursor + */ + public function showCursor() + { + echo "\x1b[?25h"; + } + + /** + * Hide console cursor + */ + public function hideCursor() + { + echo "\x1b[?25l"; + } + + /** + * Set cursor position + * @param int $x + * @param int $y + */ + public function setPos($x, $y) + { + echo "\x1b[" . $y . ';' . $x . 'f'; + } + + /** + * Prepare a string that will be rendered in color. + * + * @param string $string + * @param int $color + * @param null|int $bgColor + * @throws Exception\BadMethodCallException + * @return string + */ + public function colorize($string, $color = null, $bgColor = null) + { + $color = $this->getColorCode($color, 'fg'); + $bgColor = $this->getColorCode($bgColor, 'bg'); + return ($color !== null ? "\x1b[" . $color . 'm' : '') + . ($bgColor !== null ? "\x1b[" . $bgColor . 'm' : '') + . $string + . "\x1b[22;39m\x1b[0;49m"; + } + + /** + * Change current drawing color. + * + * @param int $color + * @throws Exception\BadMethodCallException + */ + public function setColor($color) + { + $color = $this->getColorCode($color, 'fg'); + echo "\x1b[" . $color . 'm'; + } + + /** + * Change current drawing background color + * + * @param int $bgColor + * @throws Exception\BadMethodCallException + */ + public function setBgColor($bgColor) + { + $bgColor = $this->getColorCode($bgColor, 'bg'); + echo "\x1b[" . ($bgColor) . 'm'; + } + + /** + * Reset color to console default. + */ + public function resetColor() + { + echo "\x1b[0;49m"; // reset bg color + echo "\x1b[22;39m"; // reset fg bold, bright and faint + echo "\x1b[25;39m"; // reset fg blink + echo "\x1b[24;39m"; // reset fg underline + } + + /** + * Set Console charset to use. + * + * @param Charset\CharsetInterface $charset + */ + public function setCharset(Charset\CharsetInterface $charset) + { + $this->charset = $charset; + } + + /** + * Get charset currently in use by this adapter. + * + * @return Charset\CharsetInterface $charset + */ + public function getCharset() + { + if ($this->charset === null) { + $this->charset = $this->getDefaultCharset(); + } + + return $this->charset; + } + + /** + * @return Charset\CharsetInterface + */ + public function getDefaultCharset() + { + if ($this->isUtf8()) { + return new Charset\Utf8; + } + return new Charset\DECSG(); + } + + /** + * Read a single character from the console input + * + * @param string|null $mask A list of allowed chars + * @return string + */ + public function readChar($mask = null) + { + $this->setTTYMode('-icanon -echo'); + + $stream = fopen('php://stdin', 'rb'); + do { + $char = fgetc($stream); + } while (strlen($char) !== 1 || ($mask !== null && false === strstr($mask, $char))); + fclose($stream); + + $this->restoreTTYMode(); + return $char; + } + + /** + * Reset color to console default. + */ + public function clear() + { + echo "\x1b[2J"; // reset bg color + $this->setPos(1, 1); // reset cursor position + } + + /** + * Restore TTY (Console) mode to previous value. + * + * @return void + */ + protected function restoreTTYMode() + { + if ($this->lastTTYMode === null) { + return; + } + + shell_exec('stty ' . escapeshellarg($this->lastTTYMode)); + } + + /** + * Change TTY (Console) mode + * + * @link http://en.wikipedia.org/wiki/Stty + * @param string $mode + */ + protected function setTTYMode($mode) + { + // Store last mode + $this->lastTTYMode = trim(`stty -g`); + + // Set new mode + shell_exec('stty '.escapeshellcmd($mode)); + } + + /** + * Get the final color code and throw exception on error + * + * @param null|int|Xterm256 $color + * @param string $type (optional) Foreground 'fg' or background 'bg'. + * @throws Exception\BadMethodCallException + * @return string + */ + protected function getColorCode($color, $type = 'fg') + { + if ($color instanceof Xterm256) { + $r = new ReflectionClass($color); + $code = $r->getStaticPropertyValue('color'); + if ($type == 'fg') { + $code = sprintf($code, $color::FOREGROUND); + } else { + $code = sprintf($code, $color::BACKGROUND); + } + return $code; + } + + if ($color !== null) { + if (!isset(static::$ansiColorMap[$type][$color])) { + throw new Exception\BadMethodCallException(sprintf( + 'Unknown color "%s". Please use one of the Zend\Console\ColorInterface constants ' + . 'or use Zend\Console\Color\Xterm256::calculate', + $color + )); + } + + return static::$ansiColorMap[$type][$color]; + } + + return null; + } +} diff --git a/library/Zend/Console/Adapter/Virtual.php b/library/Zend/Console/Adapter/Virtual.php new file mode 100755 index 0000000000..f1b1eb95e3 --- /dev/null +++ b/library/Zend/Console/Adapter/Virtual.php @@ -0,0 +1,176 @@ + 0) { + return $width; + } + + // Try to read console size from "mode" command + if ($this->modeResult === null) { + $this->runProbeCommand(); + } + + if (preg_match('/Columns\:\s+(\d+)/', $this->modeResult, $matches)) { + $width = $matches[1]; + } else { + $width = parent::getWidth(); + } + + return $width; + } + + /** + * Determine and return current console height. + * + * @return false|int + */ + public function getHeight() + { + static $height; + if ($height > 0) { + return $height; + } + + // Try to read console size from "mode" command + if ($this->modeResult === null) { + $this->runProbeCommand(); + } + + if (preg_match('/Rows\:\s+(\d+)/', $this->modeResult, $matches)) { + $height = $matches[1]; + } else { + $height = parent::getHeight(); + } + + return $height; + } + + /** + * Run and store the results of mode command + * + * @return void + */ + protected function runProbeCommand() + { + exec('mode', $output, $return); + if ($return || !count($output)) { + $this->modeResult = ''; + } else { + $this->modeResult = trim(implode('', $output)); + } + } + + /** + * Check if console is UTF-8 compatible + * + * @return bool + */ + public function isUtf8() + { + // Try to read code page info from "mode" command + if ($this->modeResult === null) { + $this->runProbeCommand(); + } + + if (preg_match('/Code page\:\s+(\d+)/', $this->modeResult, $matches)) { + return (int) $matches[1] == 65001; + } + + return false; + } + + /** + * Return current console window title. + * + * @return string + */ + public function getTitle() + { + // Try to use powershell to retrieve console window title + exec('powershell -command "write $Host.UI.RawUI.WindowTitle"', $output, $result); + if ($result || !$output) { + return ''; + } + + return trim($output, "\r\n"); + } + + /** + * Set Console charset to use. + * + * @param Charset\CharsetInterface $charset + */ + public function setCharset(Charset\CharsetInterface $charset) + { + $this->charset = $charset; + } + + /** + * Get charset currently in use by this adapter. + * + * @return Charset\CharsetInterface $charset + */ + public function getCharset() + { + if ($this->charset === null) { + $this->charset = $this->getDefaultCharset(); + } + + return $this->charset; + } + + /** + * @return Charset\AsciiExtended + */ + public function getDefaultCharset() + { + return new Charset\AsciiExtended; + } + + /** + * Switch to UTF mode + * + * @return void + */ + protected function switchToUtf8() + { + shell_exec('mode con cp select=65001'); + } +} diff --git a/library/Zend/Console/Adapter/Windows.php b/library/Zend/Console/Adapter/Windows.php new file mode 100755 index 0000000000..c1bf8b734b --- /dev/null +++ b/library/Zend/Console/Adapter/Windows.php @@ -0,0 +1,356 @@ + 0) { + return $width; + } + + // Try to read console size from "mode" command + if ($this->probeResult === null) { + $this->runProbeCommand(); + } + + if (count($this->probeResult) && (int) $this->probeResult[0]) { + $width = (int) $this->probeResult[0]; + } else { + $width = parent::getWidth(); + } + + return $width; + } + + /** + * Determine and return current console height. + * + * @return int + */ + public function getHeight() + { + static $height; + if ($height > 0) { + return $height; + } + + // Try to read console size from "mode" command + if ($this->probeResult === null) { + $this->runProbeCommand(); + } + + if (count($this->probeResult) && (int) $this->probeResult[1]) { + $height = (int) $this->probeResult[1]; + } else { + $height = parent::getheight(); + } + + return $height; + } + + /** + * Probe for system capabilities and cache results + * + * Run a Windows Powershell command that determines parameters of console window. The command is fed through + * standard input (with echo) to prevent Powershell from creating a sub-thread and hanging PHP when run through + * a debugger/IDE. + * + * @return void + */ + protected function runProbeCommand() + { + exec( + 'echo $size = $Host.ui.rawui.windowsize; write $($size.width) $($size.height) | powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command -', + $output, + $return + ); + if ($return || empty($output)) { + $this->probeResult = ''; + } else { + $this->probeResult = $output; + } + } + + /** + * Run and cache results of mode command + * + * @return void + */ + protected function runModeCommand() + { + exec('mode', $output, $return); + if ($return || !count($output)) { + $this->modeResult = ''; + } else { + $this->modeResult = trim(implode('', $output)); + } + } + + /** + * Check if console is UTF-8 compatible + * + * @return bool + */ + public function isUtf8() + { + // Try to read code page info from "mode" command + if ($this->modeResult === null) { + $this->runModeCommand(); + } + + if (preg_match('/Code page\:\s+(\d+)/', $this->modeResult, $matches)) { + return (int) $matches[1] == 65001; + } + + return false; + } + + /** + * Return current console window title. + * + * @return string + */ + public function getTitle() + { + // Try to use powershell to retrieve console window title + exec('powershell -command "write $Host.UI.RawUI.WindowTitle"', $output, $result); + if ($result || !$output) { + return ''; + } + + return trim($output, "\r\n"); + } + + /** + * Set Console charset to use. + * + * @param Charset\CharsetInterface $charset + */ + public function setCharset(Charset\CharsetInterface $charset) + { + $this->charset = $charset; + } + + /** + * Get charset currently in use by this adapter. + * + * @return Charset\CharsetInterface $charset + */ + public function getCharset() + { + if ($this->charset === null) { + $this->charset = $this->getDefaultCharset(); + } + + return $this->charset; + } + + /** + * @return Charset\AsciiExtended + */ + public function getDefaultCharset() + { + return new Charset\AsciiExtended; + } + + /** + * Switch to utf-8 encoding + * + * @return void + */ + protected function switchToUtf8() + { + shell_exec('mode con cp select=65001'); + } + + /** + * Clear console screen + */ + public function clear() + { + // Attempt to clear the screen using PowerShell command + exec("powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command Clear-Host", $output, $return); + + if ($return) { + // Could not run powershell... fall back to filling the buffer with newlines + echo str_repeat("\r\n", $this->getHeight()); + } + } + + /** + * Clear line at cursor position + */ + public function clearLine() + { + echo "\r" . str_repeat(' ', $this->getWidth()) . "\r"; + } + + /** + * Read a single character from the console input + * + * @param string|null $mask A list of allowed chars + * @throws Exception\RuntimeException + * @return string + */ + public function readChar($mask = null) + { + // Decide if we can use `choice` tool + $useChoice = $mask !== null && preg_match('/^[a-zA-Z0-9]+$/D', $mask); + + if ($useChoice) { + // Use Windows 95+ "choice" command, which allows for reading a + // single character matching a mask, but is limited to lower ASCII + // range. + do { + exec('choice /n /cs /c:' . $mask, $output, $return); + if ($return == 255 || $return < 1 || $return > strlen($mask)) { + throw new Exception\RuntimeException('"choice" command failed to run. Are you using Windows XP or newer?'); + } + + // Fetch the char from mask + $char = substr($mask, $return - 1, 1); + } while ("" === $char || ($mask !== null && false === strstr($mask, $char))); + + return $char; + } + + // Try to use PowerShell, giving it console access. Because PowersShell + // interpreter can take a short while to load, we are emptying the + // whole keyboard buffer and picking the last key that has been pressed + // before or after PowerShell command has started. The ASCII code for + // that key is then converted to a character. + if ($mask === null) { + exec( + 'powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command "' + . 'while ($Host.UI.RawUI.KeyAvailable) {$key = $Host.UI.RawUI.ReadKey(\'NoEcho,IncludeKeyDown\');}' + . 'write $key.VirtualKeyCode;' + . '"', + $result, + $return + ); + + // Retrieve char from the result. + $char = !empty($result) ? implode('', $result) : null; + + if (!empty($char) && !$return) { + // We have obtained an ASCII code, convert back to a char ... + $char = chr($char); + + // ... and return it... + return $char; + } + } else { + // Windows and DOS will return carriage-return char (ASCII 13) when + // the user presses [ENTER] key, but Console Adapter user might + // have provided a \n Newline (ASCII 10) in the mask, to allow [ENTER]. + // We are going to replace all CR with NL to conform. + $mask = strtr($mask, "\n", "\r"); + + // Prepare a list of ASCII codes from mask chars + $asciiMask = array_map(function ($char) { + return ord($char); + }, str_split($mask)); + $asciiMask = array_unique($asciiMask); + + // Char mask filtering is now handled by the PowerShell itself, + // because it's a much faster method than invoking PS interpreter + // after each mismatch. The command should return ASCII code of a + // matching key. + $result = $return = null; + + exec( + 'powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command "' + . '[int[]] $mask = ' . join(',', $asciiMask) . ';' + . 'do {' + . '$key = $Host.UI.RawUI.ReadKey(\'NoEcho,IncludeKeyDown\').VirtualKeyCode;' + . '} while ( !($mask -contains $key) );' + . 'write $key;' + . '"', + $result, + $return + ); + + $char = !empty($result) ? trim(implode('', $result)) : null; + + if (!$return && $char && ($mask === null || in_array($char, $asciiMask))) { + // Normalize CR to LF + if ($char == 13) { + $char = 10; + } + + // Convert to a char + $char = chr($char); + + // ... and return it... + return $char; + } + } + + // Fall back to standard input, which on Windows does not allow reading + // a single character. This is a limitation of Windows streams + // implementation (not PHP) and this behavior cannot be changed with a + // command like "stty", known to POSIX systems. + $stream = fopen('php://stdin', 'rb'); + do { + $char = fgetc($stream); + $char = substr(trim($char), 0, 1); + } while (!$char || ($mask !== null && !stristr($mask, $char))); + fclose($stream); + + return $char; + } + + /** + * Read a single line from the console input. + * + * @param int $maxLength Maximum response length + * @return string + */ + public function readLine($maxLength = 2048) + { + $f = fopen('php://stdin', 'r'); + $line = rtrim(fread($f, $maxLength), "\r\n"); + fclose($f); + + return $line; + } +} diff --git a/library/Zend/Console/Adapter/WindowsAnsicon.php b/library/Zend/Console/Adapter/WindowsAnsicon.php new file mode 100755 index 0000000000..3519a9e21e --- /dev/null +++ b/library/Zend/Console/Adapter/WindowsAnsicon.php @@ -0,0 +1,302 @@ + 0) { + return $width; + } + + // Try to read console size from ANSICON env var + if (preg_match('/\((\d+)x/', getenv('ANSICON'), $matches)) { + $width = $matches[1]; + } else { + $width = AbstractAdapter::getWidth(); + } + + return $width; + } + + /** + * Determine and return current console height. + * + * @return false|int + */ + public function getHeight() + { + static $height; + if ($height > 0) { + return $height; + } + + // Try to read console size from ANSICON env var + if (preg_match('/\(\d+x(\d+)/', getenv('ANSICON'), $matches)) { + $height = $matches[1]; + } else { + $height = AbstractAdapter::getHeight(); + } + return $height; + } + + /** + * Run and cache results of mode command + * + * @return void + */ + protected function runModeCommand() + { + exec('mode', $output, $return); + if ($return || !count($output)) { + $this->modeResult = ''; + } else { + $this->modeResult = trim(implode('', $output)); + } + } + + /** + * Check if console is UTF-8 compatible + * + * @return bool + */ + public function isUtf8() + { + // Try to read code page info from "mode" command + if ($this->modeResult === null) { + $this->runModeCommand(); + } + + if (preg_match('/Code page\:\s+(\d+)/', $this->modeResult, $matches)) { + return (int) $matches[1] == 65001; + } + + return false; + } + + /** + * Return current console window title. + * + * @return string + */ + public function getTitle() + { + // Try to use powershell to retrieve console window title + exec('powershell -command "write $Host.UI.RawUI.WindowTitle"', $output, $result); + if ($result || !$output) { + return ''; + } + + return trim($output, "\r\n"); + } + + /** + * Clear console screen + */ + public function clear() + { + echo chr(27) . '[1J' . chr(27) . '[u'; + } + + /** + * Clear line at cursor position + */ + public function clearLine() + { + echo chr(27) . '[1K'; + } + + /** + * Set Console charset to use. + * + * @param CharsetInterface $charset + */ + public function setCharset(CharsetInterface $charset) + { + $this->charset = $charset; + } + + /** + * Get charset currently in use by this adapter. + * + + * @return CharsetInterface $charset + */ + public function getCharset() + { + if ($this->charset === null) { + $this->charset = $this->getDefaultCharset(); + } + + return $this->charset; + } + + /** + * @return Charset\AsciiExtended + */ + public function getDefaultCharset() + { + return new Charset\AsciiExtended(); + } + + /** + * Read a single character from the console input + * + * @param string|null $mask A list of allowed chars + * @return string + * @throws Exception\RuntimeException + */ + public function readChar($mask = null) + { + // Decide if we can use `choice` tool + $useChoice = $mask !== null && preg_match('/^[a-zA-Z0-9]+$/D', $mask); + + if ($useChoice) { + // Use Windows 98+ "choice" command, which allows for reading a + // single character matching a mask, but is limited to lower ASCII + // range. + do { + exec('choice /n /cs /c:' . $mask, $output, $return); + if ($return == 255 || $return < 1 || $return > strlen($mask)) { + throw new Exception\RuntimeException('"choice" command failed to run. Are you using Windows XP or newer?'); + } + + // Fetch the char from mask + $char = substr($mask, $return - 1, 1); + } while ("" === $char || ($mask !== null && false === strstr($mask, $char))); + + return $char; + } + + // Try to use PowerShell, giving it console access. Because PowersShell + // interpreter can take a short while to load, we are emptying the + // whole keyboard buffer and picking the last key that has been pressed + // before or after PowerShell command has started. The ASCII code for + // that key is then converted to a character. + if ($mask === null) { + exec( + 'powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command "' + . 'while ($Host.UI.RawUI.KeyAvailable) {$key = $Host.UI.RawUI.ReadKey(\'NoEcho,IncludeKeyDown\');}' + . 'write $key.VirtualKeyCode;' + . '"', + $result, + $return + ); + + // Retrieve char from the result. + $char = !empty($result) ? implode('', $result) : null; + + if (!empty($char) && !$return) { + // We have obtained an ASCII code, convert back to a char ... + $char = chr($char); + + // ... and return it... + return $char; + } + } else { + // Windows and DOS will return carriage-return char (ASCII 13) when + // the user presses [ENTER] key, but Console Adapter user might + // have provided a \n Newline (ASCII 10) in the mask, to allow + // [ENTER]. We are going to replace all CR with NL to conform. + $mask = strtr($mask, "\n", "\r"); + + // Prepare a list of ASCII codes from mask chars + $asciiMask = array_map(function ($char) { + return ord($char); + }, str_split($mask)); + $asciiMask = array_unique($asciiMask); + + // Char mask filtering is now handled by the PowerShell itself, + // because it's a much faster method than invoking PS interpreter + // after each mismatch. The command should return ASCII code of a + // matching key. + $result = $return = null; + exec( + 'powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command "' + . '[int[]] $mask = '.join(',', $asciiMask).';' + . 'do {' + . '$key = $Host.UI.RawUI.ReadKey(\'NoEcho,IncludeKeyDown\').VirtualKeyCode;' + . '} while ( !($mask -contains $key) );' + . 'write $key;' + . '"', + $result, + $return + ); + + $char = !empty($result) ? trim(implode('', $result)) : null; + + if (!$return && $char && ($mask === null || in_array($char, $asciiMask))) { + // We have obtained an ASCII code, check if it is a carriage + // return and normalize it as needed + if ($char == 13) { + $char = 10; + } + + // Convert to a character + $char = chr($char); + + // ... and return it... + return $char; + } + } + + // Fall back to standard input, which on Windows does not allow reading + // a single character. This is a limitation of Windows streams + // implementation (not PHP) and this behavior cannot be changed with a + // command like "stty", known to POSIX systems. + $stream = fopen('php://stdin', 'rb'); + do { + $char = fgetc($stream); + $char = substr(trim($char), 0, 1); + } while (!$char || ($mask !== null && !stristr($mask, $char))); + fclose($stream); + + return $char; + } +} diff --git a/library/Zend/Console/CONTRIBUTING.md b/library/Zend/Console/CONTRIBUTING.md new file mode 100755 index 0000000000..e77f5d2d5b --- /dev/null +++ b/library/Zend/Console/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# CONTRIBUTING + +Please don't open pull requests against this repository, please use https://github.com/zendframework/zf2. \ No newline at end of file diff --git a/library/Zend/Console/Charset/Ascii.php b/library/Zend/Console/Charset/Ascii.php new file mode 100755 index 0000000000..0cc3261488 --- /dev/null +++ b/library/Zend/Console/Charset/Ascii.php @@ -0,0 +1,48 @@ + 0 ? (int) $val : 0; + }, $hex); + + $dhex = array_map('hexdec', $hex); + + if (array_fill(0, 3, $dhex[0]) === $dhex && (int) substr($dhex[0], -1) === 8) { + $x11 = 232 + (int) floor($dhex[0]/10); + return new static($x11); + } + + $x11 = $ahex[0] * 36 + $ahex[1] * 6 + $ahex[2] + 16; + + return new static($x11); + } +} diff --git a/library/Zend/Console/ColorInterface.php b/library/Zend/Console/ColorInterface.php new file mode 100755 index 0000000000..b2cc770b4e --- /dev/null +++ b/library/Zend/Console/ColorInterface.php @@ -0,0 +1,34 @@ +setCharset(new $className()); + } + + return static::$instance; + } + + /** + * Reset the console instance + */ + public static function resetInstance() + { + static::$instance = null; + } + + /** + * Check if currently running under MS Windows + * + * @see http://stackoverflow.com/questions/738823/possible-values-for-php-os + * @return bool + */ + public static function isWindows() + { + return + (defined('PHP_OS') && (substr_compare(PHP_OS, 'win', 0, 3, true) === 0)) || + (getenv('OS') != false && substr_compare(getenv('OS'), 'windows', 0, 7, true)) + ; + } + + /** + * Check if running under MS Windows Ansicon + * + * @return bool + */ + public static function isAnsicon() + { + return getenv('ANSICON') !== false; + } + + /** + * Check if running in a console environment (CLI) + * + * By default, returns value of PHP_SAPI global constant. If $isConsole is + * set, and a boolean value, that value will be returned. + * + * @return bool + */ + public static function isConsole() + { + if (null === static::$isConsole) { + static::$isConsole = (PHP_SAPI == 'cli'); + } + return static::$isConsole; + } + + /** + * Override the "is console environment" flag + * + * @param null|bool $flag + */ + public static function overrideIsConsole($flag) + { + if (null != $flag) { + $flag = (bool) $flag; + } + static::$isConsole = $flag; + } + + /** + * Try to detect best matching adapter + * @return string|null + */ + public static function detectBestAdapter() + { + // Check if we are in a console environment + if (!static::isConsole()) { + return null; + } + + // Check if we're on windows + if (static::isWindows()) { + if (static::isAnsicon()) { + $className = __NAMESPACE__ . '\Adapter\WindowsAnsicon'; + } else { + $className = __NAMESPACE__ . '\Adapter\Windows'; + } + + return $className; + } + + // Default is a Posix console + $className = __NAMESPACE__ . '\Adapter\Posix'; + return $className; + } + + /** + * Pass-thru static call to current AdapterInterface instance. + * + * @param $funcName + * @param $arguments + * @return mixed + */ + public static function __callStatic($funcName, $arguments) + { + $instance = static::getInstance(); + return call_user_func_array(array($instance, $funcName), $arguments); + } +} diff --git a/library/Zend/Console/Exception/BadMethodCallException.php b/library/Zend/Console/Exception/BadMethodCallException.php new file mode 100755 index 0000000000..aa650fc049 --- /dev/null +++ b/library/Zend/Console/Exception/BadMethodCallException.php @@ -0,0 +1,14 @@ +usage = $usage; + parent::__construct($message); + } + + /** + * Returns the usage + * + * @return string + */ + public function getUsageMessage() + { + return $this->usage; + } +} diff --git a/library/Zend/Console/Getopt.php b/library/Zend/Console/Getopt.php new file mode 100755 index 0000000000..12493264ad --- /dev/null +++ b/library/Zend/Console/Getopt.php @@ -0,0 +1,1056 @@ + self::MODE_ZEND, + self::CONFIG_DASHDASH => true, + self::CONFIG_IGNORECASE => false, + self::CONFIG_PARSEALL => true, + self::CONFIG_CUMULATIVE_PARAMETERS => false, + self::CONFIG_CUMULATIVE_FLAGS => false, + self::CONFIG_PARAMETER_SEPARATOR => null, + self::CONFIG_FREEFORM_FLAGS => false, + self::CONFIG_NUMERIC_FLAGS => false + ); + + /** + * Stores the command-line arguments for the calling application. + * + * @var array + */ + protected $argv = array(); + + /** + * Stores the name of the calling application. + * + * @var string + */ + protected $progname = ''; + + /** + * Stores the list of legal options for this application. + * + * @var array + */ + protected $rules = array(); + + /** + * Stores alternate spellings of legal options. + * + * @var array + */ + protected $ruleMap = array(); + + /** + * Stores options given by the user in the current invocation + * of the application, as well as parameters given in options. + * + * @var array + */ + protected $options = array(); + + /** + * Stores the command-line arguments other than options. + * + * @var array + */ + protected $remainingArgs = array(); + + /** + * State of the options: parsed or not yet parsed? + * + * @var bool + */ + protected $parsed = false; + + /** + * A list of callbacks to call when a particular option is present. + * + * @var array + */ + protected $optionCallbacks = array(); + + /** + * The constructor takes one to three parameters. + * + * The first parameter is $rules, which may be a string for + * gnu-style format, or a structured array for Zend-style format. + * + * The second parameter is $argv, and it is optional. If not + * specified, $argv is inferred from the global argv. + * + * The third parameter is an array of configuration parameters + * to control the behavior of this instance of Getopt; it is optional. + * + * @param array $rules + * @param array $argv + * @param array $getoptConfig + * @throws Exception\InvalidArgumentException + */ + public function __construct($rules, $argv = null, $getoptConfig = array()) + { + if (!isset($_SERVER['argv'])) { + $errorDescription = (ini_get('register_argc_argv') == false) + ? "argv is not available, because ini option 'register_argc_argv' is set Off" + : '$_SERVER["argv"] is not set, but Zend\Console\Getopt cannot work without this information.'; + throw new Exception\InvalidArgumentException($errorDescription); + } + + $this->progname = $_SERVER['argv'][0]; + $this->setOptions($getoptConfig); + $this->addRules($rules); + if (!is_array($argv)) { + $argv = array_slice($_SERVER['argv'], 1); + } + if (isset($argv)) { + $this->addArguments((array) $argv); + } + } + + /** + * Return the state of the option seen on the command line of the + * current application invocation. This function returns true, or the + * parameter to the option, if any. If the option was not given, + * this function returns null. + * + * The magic __get method works in the context of naming the option + * as a virtual member of this class. + * + * @param string $key + * @return string + */ + public function __get($key) + { + return $this->getOption($key); + } + + /** + * Test whether a given option has been seen. + * + * @param string $key + * @return bool + */ + public function __isset($key) + { + $this->parse(); + if (isset($this->ruleMap[$key])) { + $key = $this->ruleMap[$key]; + return isset($this->options[$key]); + } + return false; + } + + /** + * Set the value for a given option. + * + * @param string $key + * @param string $value + */ + public function __set($key, $value) + { + $this->parse(); + if (isset($this->ruleMap[$key])) { + $key = $this->ruleMap[$key]; + $this->options[$key] = $value; + } + } + + /** + * Return the current set of options and parameters seen as a string. + * + * @return string + */ + public function __toString() + { + return $this->toString(); + } + + /** + * Unset an option. + * + * @param string $key + */ + public function __unset($key) + { + $this->parse(); + if (isset($this->ruleMap[$key])) { + $key = $this->ruleMap[$key]; + unset($this->options[$key]); + } + } + + /** + * Define additional command-line arguments. + * These are appended to those defined when the constructor was called. + * + * @param array $argv + * @throws Exception\InvalidArgumentException When not given an array as parameter + * @return self + */ + public function addArguments($argv) + { + if (!is_array($argv)) { + throw new Exception\InvalidArgumentException("Parameter #1 to addArguments should be an array"); + } + $this->argv = array_merge($this->argv, $argv); + $this->parsed = false; + return $this; + } + + /** + * Define full set of command-line arguments. + * These replace any currently defined. + * + * @param array $argv + * @throws Exception\InvalidArgumentException When not given an array as parameter + * @return self + */ + public function setArguments($argv) + { + if (!is_array($argv)) { + throw new Exception\InvalidArgumentException("Parameter #1 to setArguments should be an array"); + } + $this->argv = $argv; + $this->parsed = false; + return $this; + } + + /** + * Define multiple configuration options from an associative array. + * These are not program options, but properties to configure + * the behavior of Zend\Console\Getopt. + * + * @param array $getoptConfig + * @return self + */ + public function setOptions($getoptConfig) + { + if (isset($getoptConfig)) { + foreach ($getoptConfig as $key => $value) { + $this->setOption($key, $value); + } + } + return $this; + } + + /** + * Define one configuration option as a key/value pair. + * These are not program options, but properties to configure + * the behavior of Zend\Console\Getopt. + * + * @param string $configKey + * @param string $configValue + * @return self + */ + public function setOption($configKey, $configValue) + { + if ($configKey !== null) { + $this->getoptConfig[$configKey] = $configValue; + } + return $this; + } + + /** + * Define additional option rules. + * These are appended to the rules defined when the constructor was called. + * + * @param array $rules + * @return self + */ + public function addRules($rules) + { + $ruleMode = $this->getoptConfig['ruleMode']; + switch ($this->getoptConfig['ruleMode']) { + case self::MODE_ZEND: + if (is_array($rules)) { + $this->_addRulesModeZend($rules); + break; + } + // intentional fallthrough + case self::MODE_GNU: + $this->_addRulesModeGnu($rules); + break; + default: + /** + * Call addRulesModeFoo() for ruleMode 'foo'. + * The developer should subclass Getopt and + * provide this method. + */ + $method = '_addRulesMode' . ucfirst($ruleMode); + $this->$method($rules); + } + $this->parsed = false; + return $this; + } + + /** + * Return the current set of options and parameters seen as a string. + * + * @return string + */ + public function toString() + { + $this->parse(); + $s = array(); + foreach ($this->options as $flag => $value) { + $s[] = $flag . '=' . ($value === true ? 'true' : $value); + } + return implode(' ', $s); + } + + /** + * Return the current set of options and parameters seen + * as an array of canonical options and parameters. + * + * Clusters have been expanded, and option aliases + * have been mapped to their primary option names. + * + * @return array + */ + public function toArray() + { + $this->parse(); + $s = array(); + foreach ($this->options as $flag => $value) { + $s[] = $flag; + if ($value !== true) { + $s[] = $value; + } + } + return $s; + } + + /** + * Return the current set of options and parameters seen in Json format. + * + * @return string + */ + public function toJson() + { + $this->parse(); + $j = array(); + foreach ($this->options as $flag => $value) { + $j['options'][] = array( + 'option' => array( + 'flag' => $flag, + 'parameter' => $value + ) + ); + } + + $json = \Zend\Json\Json::encode($j); + return $json; + } + + /** + * Return the current set of options and parameters seen in XML format. + * + * @return string + */ + public function toXml() + { + $this->parse(); + $doc = new \DomDocument('1.0', 'utf-8'); + $optionsNode = $doc->createElement('options'); + $doc->appendChild($optionsNode); + foreach ($this->options as $flag => $value) { + $optionNode = $doc->createElement('option'); + $optionNode->setAttribute('flag', utf8_encode($flag)); + if ($value !== true) { + $optionNode->setAttribute('parameter', utf8_encode($value)); + } + $optionsNode->appendChild($optionNode); + } + $xml = $doc->saveXML(); + return $xml; + } + + /** + * Return a list of options that have been seen in the current argv. + * + * @return array + */ + public function getOptions() + { + $this->parse(); + return array_keys($this->options); + } + + /** + * Return the state of the option seen on the command line of the + * current application invocation. + * + * This function returns true, or the parameter value to the option, if any. + * If the option was not given, this function returns false. + * + * @param string $flag + * @return mixed + */ + public function getOption($flag) + { + $this->parse(); + if ($this->getoptConfig[self::CONFIG_IGNORECASE]) { + $flag = strtolower($flag); + } + if (isset($this->ruleMap[$flag])) { + $flag = $this->ruleMap[$flag]; + if (isset($this->options[$flag])) { + return $this->options[$flag]; + } + } + return null; + } + + /** + * Return the arguments from the command-line following all options found. + * + * @return array + */ + public function getRemainingArgs() + { + $this->parse(); + return $this->remainingArgs; + } + + public function getArguments() + { + $result = $this->getRemainingArgs(); + foreach ($this->getOptions() as $option) { + $result[$option] = $this->getOption($option); + } + return $result; + } + + /** + * Return a useful option reference, formatted for display in an + * error message. + * + * Note that this usage information is provided in most Exceptions + * generated by this class. + * + * @return string + */ + public function getUsageMessage() + { + $usage = "Usage: {$this->progname} [ options ]\n"; + $maxLen = 20; + $lines = array(); + foreach ($this->rules as $rule) { + if (isset($rule['isFreeformFlag'])) { + continue; + } + $flags = array(); + if (is_array($rule['alias'])) { + foreach ($rule['alias'] as $flag) { + $flags[] = (strlen($flag) == 1 ? '-' : '--') . $flag; + } + } + $linepart['name'] = implode('|', $flags); + if (isset($rule['param']) && $rule['param'] != 'none') { + $linepart['name'] .= ' '; + switch ($rule['param']) { + case 'optional': + $linepart['name'] .= "[ <{$rule['paramType']}> ]"; + break; + case 'required': + $linepart['name'] .= "<{$rule['paramType']}>"; + break; + } + } + if (strlen($linepart['name']) > $maxLen) { + $maxLen = strlen($linepart['name']); + } + $linepart['help'] = ''; + if (isset($rule['help'])) { + $linepart['help'] .= $rule['help']; + } + $lines[] = $linepart; + } + foreach ($lines as $linepart) { + $usage .= sprintf( + "%s %s\n", + str_pad($linepart['name'], $maxLen), + $linepart['help'] + ); + } + return $usage; + } + + /** + * Define aliases for options. + * + * The parameter $aliasMap is an associative array + * mapping option name (short or long) to an alias. + * + * @param array $aliasMap + * @throws Exception\ExceptionInterface + * @return self + */ + public function setAliases($aliasMap) + { + foreach ($aliasMap as $flag => $alias) { + if ($this->getoptConfig[self::CONFIG_IGNORECASE]) { + $flag = strtolower($flag); + $alias = strtolower($alias); + } + if (!isset($this->ruleMap[$flag])) { + continue; + } + $flag = $this->ruleMap[$flag]; + if (isset($this->rules[$alias]) || isset($this->ruleMap[$alias])) { + $o = (strlen($alias) == 1 ? '-' : '--') . $alias; + throw new Exception\InvalidArgumentException("Option \"$o\" is being defined more than once."); + } + $this->rules[$flag]['alias'][] = $alias; + $this->ruleMap[$alias] = $flag; + } + return $this; + } + + /** + * Define help messages for options. + * + * The parameter $helpMap is an associative array + * mapping option name (short or long) to the help string. + * + * @param array $helpMap + * @return self + */ + public function setHelp($helpMap) + { + foreach ($helpMap as $flag => $help) { + if (!isset($this->ruleMap[$flag])) { + continue; + } + $flag = $this->ruleMap[$flag]; + $this->rules[$flag]['help'] = $help; + } + return $this; + } + + /** + * Parse command-line arguments and find both long and short + * options. + * + * Also find option parameters, and remaining arguments after + * all options have been parsed. + * + * @return self + */ + public function parse() + { + if ($this->parsed === true) { + return $this; + } + + $argv = $this->argv; + $this->options = array(); + $this->remainingArgs = array(); + while (count($argv) > 0) { + if ($argv[0] == '--') { + array_shift($argv); + if ($this->getoptConfig[self::CONFIG_DASHDASH]) { + $this->remainingArgs = array_merge($this->remainingArgs, $argv); + break; + } + } + if (substr($argv[0], 0, 2) == '--') { + $this->_parseLongOption($argv); + } elseif (substr($argv[0], 0, 1) == '-' && ('-' != $argv[0] || count($argv) >1)) { + $this->_parseShortOptionCluster($argv); + } elseif ($this->getoptConfig[self::CONFIG_PARSEALL]) { + $this->remainingArgs[] = array_shift($argv); + } else { + /* + * We should put all other arguments in remainingArgs and stop parsing + * since CONFIG_PARSEALL is false. + */ + $this->remainingArgs = array_merge($this->remainingArgs, $argv); + break; + } + } + $this->parsed = true; + + //go through parsed args and process callbacks + $this->triggerCallbacks(); + + return $this; + } + + /** + * @param string $option The name of the property which, if present, will call the passed + * callback with the value of this parameter. + * @param callable $callback The callback that will be called for this option. The first + * parameter will be the value of getOption($option), the second + * parameter will be a reference to $this object. If the callback returns + * false then an Exception\RuntimeException will be thrown indicating that + * there is a parse issue with this option. + * + * @return self + */ + public function setOptionCallback($option, \Closure $callback) + { + $this->optionCallbacks[$option] = $callback; + + return $this; + } + + /** + * Triggers all the registered callbacks. + */ + protected function triggerCallbacks() + { + foreach ($this->optionCallbacks as $option => $callback) { + if (null === $this->getOption($option)) { + continue; + } + //make sure we've resolved the alias, if using one + if (isset($this->ruleMap[$option]) && $option = $this->ruleMap[$option]) { + if (false === $callback($this->getOption($option), $this)) { + throw new Exception\RuntimeException( + "The option $option is invalid. See usage.", + $this->getUsageMessage() + ); + } + } + } + } + + /** + * Parse command-line arguments for a single long option. + * A long option is preceded by a double '--' character. + * Long options may not be clustered. + * + * @param mixed &$argv + */ + protected function _parseLongOption(&$argv) + { + $optionWithParam = ltrim(array_shift($argv), '-'); + $l = explode('=', $optionWithParam, 2); + $flag = array_shift($l); + $param = array_shift($l); + if (isset($param)) { + array_unshift($argv, $param); + } + $this->_parseSingleOption($flag, $argv); + } + + /** + * Parse command-line arguments for short options. + * Short options are those preceded by a single '-' character. + * Short options may be clustered. + * + * @param mixed &$argv + */ + protected function _parseShortOptionCluster(&$argv) + { + $flagCluster = ltrim(array_shift($argv), '-'); + foreach (str_split($flagCluster) as $flag) { + $this->_parseSingleOption($flag, $argv); + } + } + + /** + * Parse command-line arguments for a single option. + * + * @param string $flag + * @param mixed $argv + * @throws Exception\ExceptionInterface + */ + protected function _parseSingleOption($flag, &$argv) + { + if ($this->getoptConfig[self::CONFIG_IGNORECASE]) { + $flag = strtolower($flag); + } + + // Check if this option is numeric one + if (preg_match('/^\d+$/', $flag)) { + return $this->_setNumericOptionValue($flag); + } + + if (!isset($this->ruleMap[$flag])) { + // Don't throw Exception for flag-like param in case when freeform flags are allowed + if (!$this->getoptConfig[self::CONFIG_FREEFORM_FLAGS]) { + throw new Exception\RuntimeException( + "Option \"$flag\" is not recognized.", + $this->getUsageMessage() + ); + } + + // Magic methods in future will use this mark as real flag value + $this->ruleMap[$flag] = $flag; + $realFlag = $flag; + $this->rules[$realFlag] = array( + 'param' => 'optional', + 'isFreeformFlag' => true + ); + } else { + $realFlag = $this->ruleMap[$flag]; + } + + switch ($this->rules[$realFlag]['param']) { + case 'required': + if (count($argv) > 0) { + $param = array_shift($argv); + $this->_checkParameterType($realFlag, $param); + } else { + throw new Exception\RuntimeException( + "Option \"$flag\" requires a parameter.", + $this->getUsageMessage() + ); + } + break; + case 'optional': + if (count($argv) > 0 && substr($argv[0], 0, 1) != '-') { + $param = array_shift($argv); + $this->_checkParameterType($realFlag, $param); + } else { + $param = true; + } + break; + default: + $param = true; + } + + $this->_setSingleOptionValue($realFlag, $param); + } + + /** + * Set given value as value of numeric option + * + * Throw runtime exception if this action is deny by configuration + * or no one numeric option handlers is defined + * + * @param int $value + * @throws Exception\RuntimeException + * @return void + */ + protected function _setNumericOptionValue($value) + { + if (!$this->getoptConfig[self::CONFIG_NUMERIC_FLAGS]) { + throw new Exception\RuntimeException("Using of numeric flags are deny by configuration"); + } + + if (empty($this->getoptConfig['numericFlagsOption'])) { + throw new Exception\RuntimeException("Any option for handling numeric flags are specified"); + } + + return $this->_setSingleOptionValue($this->getoptConfig['numericFlagsOption'], $value); + } + + /** + * Add relative to options' flag value + * + * If options list already has current flag as key + * and parser should follow cumulative params by configuration, + * we should to add new param to array, not to overwrite + * + * @param string $flag + * @param string $value + */ + protected function _setSingleOptionValue($flag, $value) + { + if (true === $value && $this->getoptConfig[self::CONFIG_CUMULATIVE_FLAGS]) { + // For boolean values we have to create new flag, or increase number of flags' usage count + return $this->_setBooleanFlagValue($flag); + } + + // Split multiple values, if necessary + // Filter empty values from splited array + $separator = $this->getoptConfig[self::CONFIG_PARAMETER_SEPARATOR]; + if (is_string($value) && !empty($separator) && is_string($separator) && substr_count($value, $separator)) { + $value = array_filter(explode($separator, $value)); + } + + if (!array_key_exists($flag, $this->options)) { + $this->options[$flag] = $value; + } elseif ($this->getoptConfig[self::CONFIG_CUMULATIVE_PARAMETERS]) { + $this->options[$flag] = (array) $this->options[$flag]; + array_push($this->options[$flag], $value); + } else { + $this->options[$flag] = $value; + } + } + + /** + * Set TRUE value to given flag, if this option does not exist yet + * In other case increase value to show count of flags' usage + * + * @param string $flag + */ + protected function _setBooleanFlagValue($flag) + { + $this->options[$flag] = array_key_exists($flag, $this->options) + ? (int) $this->options[$flag] + 1 + : true; + } + + /** + * Return true if the parameter is in a valid format for + * the option $flag. + * Throw an exception in most other cases. + * + * @param string $flag + * @param string $param + * @throws Exception\ExceptionInterface + * @return bool + */ + protected function _checkParameterType($flag, $param) + { + $type = 'string'; + if (isset($this->rules[$flag]['paramType'])) { + $type = $this->rules[$flag]['paramType']; + } + switch ($type) { + case 'word': + if (preg_match('/\W/', $param)) { + throw new Exception\RuntimeException( + "Option \"$flag\" requires a single-word parameter, but was given \"$param\".", + $this->getUsageMessage() + ); + } + break; + case 'integer': + if (preg_match('/\D/', $param)) { + throw new Exception\RuntimeException( + "Option \"$flag\" requires an integer parameter, but was given \"$param\".", + $this->getUsageMessage() + ); + } + break; + case 'string': + default: + break; + } + return true; + } + + /** + * Define legal options using the gnu-style format. + * + * @param string $rules + */ + protected function _addRulesModeGnu($rules) + { + $ruleArray = array(); + + /** + * Options may be single alphanumeric characters. + * Options may have a ':' which indicates a required string parameter. + * No long options or option aliases are supported in GNU style. + */ + preg_match_all('/([a-zA-Z0-9]:?)/', $rules, $ruleArray); + foreach ($ruleArray[1] as $rule) { + $r = array(); + $flag = substr($rule, 0, 1); + if ($this->getoptConfig[self::CONFIG_IGNORECASE]) { + $flag = strtolower($flag); + } + $r['alias'][] = $flag; + if (substr($rule, 1, 1) == ':') { + $r['param'] = 'required'; + $r['paramType'] = 'string'; + } else { + $r['param'] = 'none'; + } + $this->rules[$flag] = $r; + $this->ruleMap[$flag] = $flag; + } + } + + /** + * Define legal options using the Zend-style format. + * + * @param array $rules + * @throws Exception\ExceptionInterface + */ + protected function _addRulesModeZend($rules) + { + foreach ($rules as $ruleCode => $helpMessage) { + // this may have to translate the long parm type if there + // are any complaints that =string will not work (even though that use + // case is not documented) + if (in_array(substr($ruleCode, -2, 1), array('-', '='))) { + $flagList = substr($ruleCode, 0, -2); + $delimiter = substr($ruleCode, -2, 1); + $paramType = substr($ruleCode, -1); + } else { + $flagList = $ruleCode; + $delimiter = $paramType = null; + } + if ($this->getoptConfig[self::CONFIG_IGNORECASE]) { + $flagList = strtolower($flagList); + } + $flags = explode('|', $flagList); + $rule = array(); + $mainFlag = $flags[0]; + foreach ($flags as $flag) { + if (empty($flag)) { + throw new Exception\InvalidArgumentException("Blank flag not allowed in rule \"$ruleCode\"."); + } + if (strlen($flag) == 1) { + if (isset($this->ruleMap[$flag])) { + throw new Exception\InvalidArgumentException( + "Option \"-$flag\" is being defined more than once." + ); + } + $this->ruleMap[$flag] = $mainFlag; + $rule['alias'][] = $flag; + } else { + if (isset($this->rules[$flag]) || isset($this->ruleMap[$flag])) { + throw new Exception\InvalidArgumentException( + "Option \"--$flag\" is being defined more than once." + ); + } + $this->ruleMap[$flag] = $mainFlag; + $rule['alias'][] = $flag; + } + } + if (isset($delimiter)) { + switch ($delimiter) { + case self::PARAM_REQUIRED: + $rule['param'] = 'required'; + break; + case self::PARAM_OPTIONAL: + default: + $rule['param'] = 'optional'; + } + switch (substr($paramType, 0, 1)) { + case self::TYPE_WORD: + $rule['paramType'] = 'word'; + break; + case self::TYPE_INTEGER: + $rule['paramType'] = 'integer'; + break; + case self::TYPE_NUMERIC_FLAG: + $rule['paramType'] = 'numericFlag'; + $this->getoptConfig['numericFlagsOption'] = $mainFlag; + break; + case self::TYPE_STRING: + default: + $rule['paramType'] = 'string'; + } + } else { + $rule['param'] = 'none'; + } + $rule['help'] = $helpMessage; + $this->rules[$mainFlag] = $rule; + } + } +} diff --git a/library/Zend/Console/Prompt/AbstractPrompt.php b/library/Zend/Console/Prompt/AbstractPrompt.php new file mode 100755 index 0000000000..cd717cf35c --- /dev/null +++ b/library/Zend/Console/Prompt/AbstractPrompt.php @@ -0,0 +1,85 @@ +lastResponse; + } + + /** + * Return console adapter to use when showing prompt. + * + * @return ConsoleAdapter + */ + public function getConsole() + { + if (!$this->console) { + $this->console = Console::getInstance(); + } + + return $this->console; + } + + /** + * Set console adapter to use when showing prompt. + * + * @param ConsoleAdapter $adapter + */ + public function setConsole(ConsoleAdapter $adapter) + { + $this->console = $adapter; + } + + /** + * Create an instance of this prompt, show it and return response. + * + * This is a convenience method for creating statically creating prompts, i.e.: + * + * $name = Zend\Console\Prompt\Line::prompt("Enter your name: "); + * + * @return mixed + * @throws Exception\BadMethodCallException + */ + public static function prompt() + { + if (get_called_class() === __CLASS__) { + throw new Exception\BadMethodCallException( + 'Cannot call prompt() on AbstractPrompt class. Use one of the Zend\Console\Prompt\ subclasses.' + ); + } + + $refl = new ReflectionClass(get_called_class()); + $instance = $refl->newInstanceArgs(func_get_args()); + return $instance->show(); + } +} diff --git a/library/Zend/Console/Prompt/Char.php b/library/Zend/Console/Prompt/Char.php new file mode 100755 index 0000000000..b6c3051453 --- /dev/null +++ b/library/Zend/Console/Prompt/Char.php @@ -0,0 +1,186 @@ +setPromptText($promptText); + $this->setAllowEmpty($allowEmpty); + $this->setIgnoreCase($ignoreCase); + + if (null != $allowedChars) { + if ($this->ignoreCase) { + $this->setAllowedChars(strtolower($allowedChars)); + } else { + $this->setAllowedChars($allowedChars); + } + } + + $this->setEcho($echo); + } + + /** + * Show the prompt to user and return a single char. + * + * @return string + */ + public function show() + { + $this->getConsole()->write($this->promptText); + $mask = $this->getAllowedChars(); + + /** + * Normalize the mask if case is irrelevant + */ + if ($this->ignoreCase) { + $mask = strtolower($mask); // lowercase all + $mask .= strtoupper($mask); // uppercase and append + $mask = str_split($mask); // convert to array + $mask = array_unique($mask); // remove duplicates + $mask = implode("", $mask); // convert back to string + } + + /** + * Read char from console + */ + $char = $this->getConsole()->readChar($mask); + + if ($this->echo) { + echo trim($char)."\n"; + } else { + if ($this->promptText) { + echo "\n"; // skip to next line but only if we had any prompt text + } + } + + return $this->lastResponse = $char; + } + + /** + * @param bool $allowEmpty + */ + public function setAllowEmpty($allowEmpty) + { + $this->allowEmpty = (bool) $allowEmpty; + } + + /** + * @return bool + */ + public function getAllowEmpty() + { + return $this->allowEmpty; + } + + /** + * @param string $promptText + */ + public function setPromptText($promptText) + { + $this->promptText = $promptText; + } + + /** + * @return string + */ + public function getPromptText() + { + return $this->promptText; + } + + /** + * @param string $allowedChars + */ + public function setAllowedChars($allowedChars) + { + $this->allowedChars = $allowedChars; + } + + /** + * @return string + */ + public function getAllowedChars() + { + return $this->allowedChars; + } + + /** + * @param bool $ignoreCase + */ + public function setIgnoreCase($ignoreCase) + { + $this->ignoreCase = (bool) $ignoreCase; + } + + /** + * @return bool + */ + public function getIgnoreCase() + { + return $this->ignoreCase; + } + + /** + * @param bool $echo + */ + public function setEcho($echo) + { + $this->echo = (bool) $echo; + } + + /** + * @return bool + */ + public function getEcho() + { + return $this->echo; + } +} diff --git a/library/Zend/Console/Prompt/Confirm.php b/library/Zend/Console/Prompt/Confirm.php new file mode 100755 index 0000000000..f660452be7 --- /dev/null +++ b/library/Zend/Console/Prompt/Confirm.php @@ -0,0 +1,113 @@ +setPromptText($promptText); + } + + if ($yesChar !== null) { + $this->setYesChar($yesChar); + } + + if ($noChar !== null) { + $this->setNoChar($noChar); + } + } + + /** + * Show the confirmation message and return result. + * + * @return bool + */ + public function show() + { + $char = parent::show(); + if ($this->ignoreCase) { + $response = strtolower($char) === strtolower($this->yesChar); + } else { + $response = $char === $this->yesChar; + } + return $this->lastResponse = $response; + } + + /** + * @param string $noChar + */ + public function setNoChar($noChar) + { + $this->noChar = $noChar; + $this->setAllowedChars($this->yesChar . $this->noChar); + } + + /** + * @return string + */ + public function getNoChar() + { + return $this->noChar; + } + + /** + * @param string $yesChar + */ + public function setYesChar($yesChar) + { + $this->yesChar = $yesChar; + $this->setAllowedChars($this->yesChar . $this->noChar); + } + + /** + * @return string + */ + public function getYesChar() + { + return $this->yesChar; + } +} diff --git a/library/Zend/Console/Prompt/Line.php b/library/Zend/Console/Prompt/Line.php new file mode 100755 index 0000000000..7a7427d7a5 --- /dev/null +++ b/library/Zend/Console/Prompt/Line.php @@ -0,0 +1,113 @@ +setPromptText($promptText); + } + + if ($allowEmpty !== null) { + $this->setAllowEmpty($allowEmpty); + } + + if ($maxLength !== null) { + $this->setMaxLength($maxLength); + } + } + + /** + * Show the prompt to user and return the answer. + * + * @return string + */ + public function show() + { + do { + $this->getConsole()->write($this->promptText); + $line = $this->getConsole()->readLine($this->maxLength); + } while (!$this->allowEmpty && !$line); + + return $this->lastResponse = $line; + } + + /** + * @param bool $allowEmpty + */ + public function setAllowEmpty($allowEmpty) + { + $this->allowEmpty = $allowEmpty; + } + + /** + * @return bool + */ + public function getAllowEmpty() + { + return $this->allowEmpty; + } + + /** + * @param int $maxLength + */ + public function setMaxLength($maxLength) + { + $this->maxLength = $maxLength; + } + + /** + * @return int + */ + public function getMaxLength() + { + return $this->maxLength; + } + + /** + * @param string $promptText + */ + public function setPromptText($promptText) + { + $this->promptText = $promptText; + } + + /** + * @return string + */ + public function getPromptText() + { + return $this->promptText; + } +} diff --git a/library/Zend/Console/Prompt/Number.php b/library/Zend/Console/Prompt/Number.php new file mode 100755 index 0000000000..8ce5330053 --- /dev/null +++ b/library/Zend/Console/Prompt/Number.php @@ -0,0 +1,208 @@ +setPromptText($promptText); + } + + if ($allowEmpty !== null) { + $this->setAllowEmpty($allowEmpty); + } + + if ($min !== null) { + $this->setMin($min); + } + + if ($max !== null) { + $this->setMax($max); + } + + if ($allowFloat !== null) { + $this->setAllowFloat($allowFloat); + } + } + + /** + * Show the prompt to user and return the answer. + * + * @return mixed + */ + public function show() + { + /** + * Ask for a number and validate it. + */ + do { + $valid = true; + $number = parent::show(); + if ($number === "" && !$this->allowEmpty) { + $valid = false; + } elseif ($number === "") { + $number = null; + } elseif (!is_numeric($number)) { + $this->getConsole()->writeLine("$number is not a number\n"); + $valid = false; + } elseif (!$this->allowFloat && (round($number) != $number)) { + $this->getConsole()->writeLine("Please enter a non-floating number, i.e. " . round($number) . "\n"); + $valid = false; + } elseif ($this->max !== null && $number > $this->max) { + $this->getConsole()->writeLine("Please enter a number not greater than " . $this->max . "\n"); + $valid = false; + } elseif ($this->min !== null && $number < $this->min) { + $this->getConsole()->writeLine("Please enter a number not smaller than " . $this->min . "\n"); + $valid = false; + } + } while (!$valid); + + /** + * Cast proper type + */ + if ($number !== null) { + $number = $this->allowFloat ? (double) $number : (int) $number; + } + + return $this->lastResponse = $number; + } + + /** + * @param bool $allowEmpty + */ + public function setAllowEmpty($allowEmpty) + { + $this->allowEmpty = $allowEmpty; + } + + /** + * @return bool + */ + public function getAllowEmpty() + { + return $this->allowEmpty; + } + + /** + * @param int $maxLength + */ + public function setMaxLength($maxLength) + { + $this->maxLength = $maxLength; + } + + /** + * @return int + */ + public function getMaxLength() + { + return $this->maxLength; + } + + /** + * @param string $promptText + */ + public function setPromptText($promptText) + { + $this->promptText = $promptText; + } + + /** + * @return string + */ + public function getPromptText() + { + return $this->promptText; + } + + /** + * @param int $max + */ + public function setMax($max) + { + $this->max = $max; + } + + /** + * @return int + */ + public function getMax() + { + return $this->max; + } + + /** + * @param int $min + */ + public function setMin($min) + { + $this->min = $min; + } + + /** + * @return int + */ + public function getMin() + { + return $this->min; + } + + /** + * @param bool $allowFloat + */ + public function setAllowFloat($allowFloat) + { + $this->allowFloat = $allowFloat; + } + + /** + * @return bool + */ + public function getAllowFloat() + { + return $this->allowFloat; + } +} diff --git a/library/Zend/Console/Prompt/PromptInterface.php b/library/Zend/Console/Prompt/PromptInterface.php new file mode 100755 index 0000000000..da1b6215c1 --- /dev/null +++ b/library/Zend/Console/Prompt/PromptInterface.php @@ -0,0 +1,44 @@ +setPromptText($promptText); + } + + if (!count($options)) { + throw new Exception\BadMethodCallException( + 'Cannot construct a "select" prompt without any options' + ); + } + + $this->setOptions($options); + + if ($allowEmpty !== null) { + $this->setAllowEmpty($allowEmpty); + } + + if ($echo !== null) { + $this->setEcho($echo); + } + } + + /** + * Show a list of options and prompt the user to select one of them. + * + * @return string Selected option + */ + public function show() + { + // Show prompt text and available options + $console = $this->getConsole(); + $console->writeLine($this->promptText); + foreach ($this->options as $k => $v) { + $console->writeLine(' ' . $k . ') ' . $v); + } + + // Prepare mask + $mask = implode("", array_keys($this->options)); + if ($this->allowEmpty) { + $mask .= "\r\n"; + } + + // Prepare other params for parent class + $this->setAllowedChars($mask); + $oldPrompt = $this->promptText; + $oldEcho = $this->echo; + $this->echo = false; + $this->promptText = null; + + // Retrieve a single character + $response = parent::show(); + + // Restore old params + $this->promptText = $oldPrompt; + $this->echo = $oldEcho; + + // Display selected option if echo is enabled + if ($this->echo) { + if (isset($this->options[$response])) { + $console->writeLine($this->options[$response]); + } else { + $console->writeLine(); + } + } + + $this->lastResponse = $response; + return $response; + } + + /** + * Set allowed options + * + * @param array|\Traversable $options + * @throws Exception\BadMethodCallException + */ + public function setOptions($options) + { + if (!is_array($options) && !$options instanceof \Traversable) { + throw new Exception\BadMethodCallException( + 'Please specify an array or Traversable object as options' + ); + } + + if (!is_array($options)) { + $this->options = array(); + foreach ($options as $k => $v) { + $this->options[$k] = $v; + } + } else { + $this->options = $options; + } + } + + /** + * @return array + */ + public function getOptions() + { + return $this->options; + } +} diff --git a/library/Zend/Console/README.md b/library/Zend/Console/README.md new file mode 100755 index 0000000000..eb8566c0f2 --- /dev/null +++ b/library/Zend/Console/README.md @@ -0,0 +1,15 @@ +Console Component from ZF2 +========================== + +This is the Console component for ZF2. + +- File issues at https://github.com/zendframework/zf2/issues +- Create pull requests against https://github.com/zendframework/zf2 +- Documentation is at http://framework.zend.com/docs + +LICENSE +------- + +The files in this archive are released under the [Zend Framework +license](http://framework.zend.com/license), which is a 3-clause BSD license. + diff --git a/library/Zend/Console/Request.php b/library/Zend/Console/Request.php new file mode 100755 index 0000000000..57b48bb2cb --- /dev/null +++ b/library/Zend/Console/Request.php @@ -0,0 +1,197 @@ + 0) { + $this->setScriptName(array_shift($args)); + } + + /** + * Store runtime params + */ + $this->params()->fromArray($args); + $this->setContent($args); + + /** + * Store environment data + */ + $this->env()->fromArray($env); + } + + /** + * Exchange parameters object + * + * @param \Zend\Stdlib\Parameters $params + * @return Request + */ + public function setParams(Parameters $params) + { + $this->params = $params; + $this->setContent($params); + return $this; + } + + /** + * Return the container responsible for parameters + * + * @return \Zend\Stdlib\Parameters + */ + public function getParams() + { + if ($this->params === null) { + $this->params = new Parameters(); + } + + return $this->params; + } + + /** + * Return a single parameter. + * Shortcut for $request->params()->get() + * + * @param string $name Parameter name + * @param string $default (optional) default value in case the parameter does not exist + * @return mixed + */ + public function getParam($name, $default = null) + { + return $this->params()->get($name, $default); + } + + /** + * Return the container responsible for parameters + * + * @return \Zend\Stdlib\Parameters + */ + public function params() + { + return $this->getParams(); + } + + /** + * Provide an alternate Parameter Container implementation for env parameters in this object, (this is NOT the + * primary API for value setting, for that see env()) + * + * @param \Zend\Stdlib\Parameters $env + * @return \Zend\Console\Request + */ + public function setEnv(Parameters $env) + { + $this->envParams = $env; + return $this; + } + + /** + * Return a single parameter container responsible for env parameters + * + * @param string $name Parameter name + * @param string $default (optional) default value in case the parameter does not exist + * @return \Zend\Stdlib\Parameters + */ + public function getEnv($name, $default = null) + { + return $this->env()->get($name, $default); + } + + /** + * Return the parameter container responsible for env parameters + * + * @return \Zend\Stdlib\Parameters + */ + public function env() + { + if ($this->envParams === null) { + $this->envParams = new Parameters(); + } + + return $this->envParams; + } + + /** + * @return string + */ + public function toString() + { + return trim(implode(' ', $this->params()->toArray())); + } + + /** + * Allow PHP casting of this object + * + * @return string + */ + public function __toString() + { + return $this->toString(); + } + + /** + * @param string $scriptName + */ + public function setScriptName($scriptName) + { + $this->scriptName = $scriptName; + } + + /** + * @return string + */ + public function getScriptName() + { + return $this->scriptName; + } +} diff --git a/library/Zend/Console/Response.php b/library/Zend/Console/Response.php new file mode 100755 index 0000000000..75a068614c --- /dev/null +++ b/library/Zend/Console/Response.php @@ -0,0 +1,80 @@ +contentSent; + } + + /** + * Set the error level that will be returned to shell. + * + * @param int $errorLevel + * @return Response + */ + public function setErrorLevel($errorLevel) + { + $this->setMetadata('errorLevel', $errorLevel); + return $this; + } + + /** + * Get response error level that will be returned to shell. + * + * @return int|0 + */ + public function getErrorLevel() + { + return $this->getMetadata('errorLevel', 0); + } + + /** + * Send content + * + * @return Response + * @deprecated + */ + public function sendContent() + { + if ($this->contentSent()) { + return $this; + } + echo $this->getContent(); + $this->contentSent = true; + return $this; + } + + /** + * @deprecated + */ + public function send() + { + $this->sendContent(); + $errorLevel = (int) $this->getMetadata('errorLevel', 0); + exit($errorLevel); + } +} diff --git a/library/Zend/Console/RouteMatcher/DefaultRouteMatcher.php b/library/Zend/Console/RouteMatcher/DefaultRouteMatcher.php new file mode 100755 index 0000000000..a5f8fae575 --- /dev/null +++ b/library/Zend/Console/RouteMatcher/DefaultRouteMatcher.php @@ -0,0 +1,780 @@ +defaults = $defaults; + $this->constraints = $constraints; + $this->aliases = $aliases; + + if ($filters !== null) { + foreach ($filters as $name => $filter) { + if (!$filter instanceof FilterInterface) { + throw new Exception\InvalidArgumentException('Cannot use ' . gettype($filters) . ' as filter for ' . __CLASS__); + } + $this->filters[$name] = $filter; + } + } + + if ($validators !== null) { + foreach ($validators as $name => $validator) { + if (!$validator instanceof ValidatorInterface) { + throw new Exception\InvalidArgumentException('Cannot use ' . gettype($validator) . ' as validator for ' . __CLASS__); + } + $this->validators[$name] = $validator; + } + } + + $this->parts = $this->parseDefinition($route); + } + + /** + * Parse a route definition. + * + * @param string $def + * @return array + * @throws Exception\InvalidArgumentException + */ + protected function parseDefinition($def) + { + $def = trim($def); + $pos = 0; + $length = strlen($def); + $parts = array(); + $unnamedGroupCounter = 1; + + while ($pos < $length) { + /** + * Optional value param, i.e. + * [SOMETHING] + */ + if (preg_match('/\G\[(?P[A-Z][A-Z0-9\_\-]*?)\](?: +|$)/s', $def, $m, 0, $pos)) { + $item = array( + 'name' => strtolower($m['name']), + 'literal' => false, + 'required' => false, + 'positional' => true, + 'hasValue' => true, + ); + } + /** + * Mandatory value param, i.e. + * SOMETHING + */ + elseif (preg_match('/\G(?P[A-Z][A-Z0-9\_\-]*?)(?: +|$)/s', $def, $m, 0, $pos)) { + $item = array( + 'name' => strtolower($m['name']), + 'literal' => false, + 'required' => true, + 'positional' => true, + 'hasValue' => true, + ); + } + /** + * Optional literal param, i.e. + * [something] + */ + elseif (preg_match('/\G\[ *?(?P[a-zA-Z][a-zA-Z0-9\_\-]*?) *?\](?: +|$)/s', $def, $m, 0, $pos)) { + $item = array( + 'name' => $m['name'], + 'literal' => true, + 'required' => false, + 'positional' => true, + 'hasValue' => false, + ); + } + /** + * Optional value param, syntax 2, i.e. + * [] + */ + elseif (preg_match('/\G\[ *\<(?P[a-zA-Z][a-zA-Z0-9\_\-]*?)\> *\](?: +|$)/s', $def, $m, 0, $pos)) { + $item = array( + 'name' => $m['name'], + 'literal' => false, + 'required' => false, + 'positional' => true, + 'hasValue' => true, + ); + } + /** + * Mandatory value param, i.e. + * + */ + elseif (preg_match('/\G\< *(?P[a-zA-Z][a-zA-Z0-9\_\-]*?) *\>(?: +|$)/s', $def, $m, 0, $pos)) { + $item = array( + 'name' => $m['name'], + 'literal' => false, + 'required' => true, + 'positional' => true, + 'hasValue' => true, + ); + } + /** + * Mandatory literal param, i.e. + * something + */ + elseif (preg_match('/\G(?P[a-zA-Z][a-zA-Z0-9\_\-]*?)(?: +|$)/s', $def, $m, 0, $pos)) { + $item = array( + 'name' => $m['name'], + 'literal' => true, + 'required' => true, + 'positional' => true, + 'hasValue' => false, + ); + } + /** + * Mandatory long param + * --param= + * --param=whatever + */ + elseif (preg_match('/\G--(?P[a-zA-Z0-9][a-zA-Z0-9\_\-]+)(?P=\S*?)?(?: +|$)/s', $def, $m, 0, $pos)) { + $item = array( + 'name' => $m['name'], + 'short' => false, + 'literal' => false, + 'required' => true, + 'positional' => false, + 'hasValue' => !empty($m['hasValue']), + ); + } + /** + * Optional long flag + * [--param] + */ + elseif (preg_match( + '/\G\[ *?--(?P[a-zA-Z0-9][a-zA-Z0-9\_\-]+) *?\](?: +|$)/s', $def, $m, 0, $pos + )) { + $item = array( + 'name' => $m['name'], + 'short' => false, + 'literal' => false, + 'required' => false, + 'positional' => false, + 'hasValue' => false, + ); + } + /** + * Optional long param + * [--param=] + * [--param=whatever] + */ + elseif (preg_match( + '/\G\[ *?--(?P[a-zA-Z0-9][a-zA-Z0-9\_\-]+)(?P=\S*?)? *?\](?: +|$)/s', $def, $m, 0, $pos + )) { + $item = array( + 'name' => $m['name'], + 'short' => false, + 'literal' => false, + 'required' => false, + 'positional' => false, + 'hasValue' => !empty($m['hasValue']), + ); + } + /** + * Mandatory short param + * -a + * -a=i + * -a=s + * -a=w + */ + elseif (preg_match('/\G-(?P[a-zA-Z0-9])(?:=(?P[ns]))?(?: +|$)/s', $def, $m, 0, $pos)) { + $item = array( + 'name' => $m['name'], + 'short' => true, + 'literal' => false, + 'required' => true, + 'positional' => false, + 'hasValue' => !empty($m['type']) ? $m['type'] : null, + ); + } + /** + * Optional short param + * [-a] + * [-a=n] + * [-a=s] + */ + elseif (preg_match('/\G\[ *?-(?P[a-zA-Z0-9])(?:=(?P[ns]))? *?\](?: +|$)/s', $def, $m, 0, $pos)) { + $item = array( + 'name' => $m['name'], + 'short' => true, + 'literal' => false, + 'required' => false, + 'positional' => false, + 'hasValue' => !empty($m['type']) ? $m['type'] : null, + ); + } + /** + * Optional literal param alternative + * [ something | somethingElse | anotherOne ] + * [ something | somethingElse | anotherOne ]:namedGroup + */ + elseif (preg_match('/ + \G + \[ + (?P + (?: + \ *? + (?P[a-zA-Z][a-zA-Z0-9_\-]*?) + \ *? + (?:\||(?=\])) + \ *? + )+ + ) + \] + (?:\:(?P[a-zA-Z0-9]+))? + (?:\ +|$) + /sx', $def, $m, 0, $pos + ) + ) { + // extract available options + $options = preg_split('/ *\| */', trim($m['options']), 0, PREG_SPLIT_NO_EMPTY); + + // remove dupes + array_unique($options); + + // prepare item + $item = array( + 'name' => isset($m['groupName']) ? $m['groupName'] : 'unnamedGroup' . $unnamedGroupCounter++, + 'literal' => true, + 'required' => false, + 'positional' => true, + 'alternatives' => $options, + 'hasValue' => false, + ); + } + + /** + * Required literal param alternative + * ( something | somethingElse | anotherOne ) + * ( something | somethingElse | anotherOne ):namedGroup + */ + elseif (preg_match('/ + \G + \( + (?P + (?: + \ *? + (?P[a-zA-Z][a-zA-Z0-9_\-]+) + \ *? + (?:\||(?=\))) + \ *? + )+ + ) + \) + (?:\:(?P[a-zA-Z0-9]+))? + (?:\ +|$) + /sx', $def, $m, 0, $pos + )) { + // extract available options + $options = preg_split('/ *\| */', trim($m['options']), 0, PREG_SPLIT_NO_EMPTY); + + // remove dupes + array_unique($options); + + // prepare item + $item = array( + 'name' => isset($m['groupName']) ? $m['groupName']:'unnamedGroupAt' . $unnamedGroupCounter++, + 'literal' => true, + 'required' => true, + 'positional' => true, + 'alternatives' => $options, + 'hasValue' => false, + ); + } + /** + * Required long/short flag alternative + * ( --something | --somethingElse | --anotherOne | -s | -a ) + * ( --something | --somethingElse | --anotherOne | -s | -a ):namedGroup + */ + elseif (preg_match('/ + \G + \( + (?P + (?: + \ *? + \-+(?P[a-zA-Z0-9][a-zA-Z0-9_\-]*?) + \ *? + (?:\||(?=\))) + \ *? + )+ + ) + \) + (?:\:(?P[a-zA-Z0-9]+))? + (?:\ +|$) + /sx', $def, $m, 0, $pos + )) { + // extract available options + $options = preg_split('/ *\| */', trim($m['options']), 0, PREG_SPLIT_NO_EMPTY); + + // remove dupes + array_unique($options); + + // remove prefix + array_walk($options, function (&$val, $key) { + $val = ltrim($val, '-'); + }); + + // prepare item + $item = array( + 'name' => isset($m['groupName']) ? $m['groupName']:'unnamedGroupAt' . $unnamedGroupCounter++, + 'literal' => false, + 'required' => true, + 'positional' => false, + 'alternatives' => $options, + 'hasValue' => false, + ); + } + /** + * Optional flag alternative + * [ --something | --somethingElse | --anotherOne | -s | -a ] + * [ --something | --somethingElse | --anotherOne | -s | -a ]:namedGroup + */ + elseif (preg_match('/ + \G + \[ + (?P + (?: + \ *? + \-+(?P[a-zA-Z0-9][a-zA-Z0-9_\-]*?) + \ *? + (?:\||(?=\])) + \ *? + )+ + ) + \] + (?:\:(?P[a-zA-Z0-9]+))? + (?:\ +|$) + /sx', $def, $m, 0, $pos + )) { + // extract available options + $options = preg_split('/ *\| */', trim($m['options']), 0, PREG_SPLIT_NO_EMPTY); + + // remove dupes + array_unique($options); + + // remove prefix + array_walk($options, function (&$val, $key) { + $val = ltrim($val, '-'); + }); + + // prepare item + $item = array( + 'name' => isset($m['groupName']) ? $m['groupName']:'unnamedGroupAt' . $unnamedGroupCounter++, + 'literal' => false, + 'required' => false, + 'positional' => false, + 'alternatives' => $options, + 'hasValue' => false, + ); + } else { + throw new Exception\InvalidArgumentException( + 'Cannot understand Console route at "' . substr($def, $pos) . '"' + ); + } + + $pos += strlen($m[0]); + $parts[] = $item; + } + + return $parts; + } + + /** + * Returns list of names representing single parameter + * + * @param string $name + * @return string + */ + private function getAliases($name) + { + $namesToMatch = array($name); + foreach ($this->aliases as $alias => $canonical) { + if ($name == $canonical) { + $namesToMatch[] = $alias; + } + } + return $namesToMatch; + } + + /** + * Returns canonical name of a parameter + * + * @param string $name + * @return string + */ + private function getCanonicalName($name) + { + if (isset($this->aliases[$name])) { + return $this->aliases[$name]; + } + return $name; + } + + /** + * Match parameters against route passed to constructor + * + * @param array $params + * @return array|null + */ + public function match($params) + { + $matches = array(); + + /* + * Extract positional and named parts + */ + $positional = $named = array(); + foreach ($this->parts as &$part) { + if ($part['positional']) { + $positional[] = &$part; + } else { + $named[] = &$part; + } + } + + /* + * Scan for named parts inside Console params + */ + foreach ($named as &$part) { + /* + * Prepare match regex + */ + if (isset($part['alternatives'])) { + // an alternative of flags + $regex = '/^\-+(?P'; + + $alternativeAliases = array(); + foreach ($part['alternatives'] as $alternative) { + $alternativeAliases[] = '(?:' . implode('|', $this->getAliases($alternative)) . ')'; + } + + $regex .= join('|', $alternativeAliases); + + if ($part['hasValue']) { + $regex .= ')(?:\=(?P.*?)$)?$/'; + } else { + $regex .= ')$/i'; + } + } else { + // a single named flag + $name = '(?:' . implode('|', $this->getAliases($part['name'])) . ')'; + + if ($part['short'] === true) { + // short variant + if ($part['hasValue']) { + $regex = '/^\-' . $name . '(?:\=(?P.*?)$)?$/i'; + } else { + $regex = '/^\-' . $name . '$/i'; + } + } elseif ($part['short'] === false) { + // long variant + if ($part['hasValue']) { + $regex = '/^\-{2,}' . $name . '(?:\=(?P.*?)$)?$/i'; + } else { + $regex = '/^\-{2,}' . $name . '$/i'; + } + } + } + + /* + * Look for param + */ + $value = $param = null; + for ($x = 0, $count = count($params); $x < $count; $x++) { + if (preg_match($regex, $params[$x], $m)) { + // found param + $param = $params[$x]; + + // prevent further scanning of this param + array_splice($params, $x, 1); + + if (isset($m['value'])) { + $value = $m['value']; + } + + if (isset($m['name'])) { + $matchedName = $this->getCanonicalName($m['name']); + } + + break; + } + } + + + if (!$param) { + /* + * Drop out if that was a mandatory param + */ + if ($part['required']) { + return null; + } + + /* + * Continue to next positional param + */ + else { + continue; + } + } + + + /* + * Value for flags is always boolean + */ + if ($param && !$part['hasValue']) { + $value = true; + } + + /* + * Try to retrieve value if it is expected + */ + if ((null === $value || "" === $value) && $part['hasValue']) { + if ($x < count($params)+1 && isset($params[$x])) { + // retrieve value from adjacent param + $value = $params[$x]; + + // prevent further scanning of this param + array_splice($params, $x, 1); + } else { + // there are no more params available + return null; + } + } + + /* + * Validate the value against constraints + */ + if ($part['hasValue'] && isset($this->constraints[$part['name']])) { + if ( + !preg_match($this->constraints[$part['name']], $value) + ) { + // constraint failed + return null; + } + } + + /* + * Store the value + */ + if ($part['hasValue']) { + $matches[$part['name']] = $value; + } else { + $matches[$part['name']] = true; + } + + /* + * If there are alternatives, fill them + */ + if (isset($part['alternatives'])) { + if ($part['hasValue']) { + foreach ($part['alternatives'] as $alt) { + if ($alt === $matchedName && !isset($matches[$alt])) { + $matches[$alt] = $value; + } elseif (!isset($matches[$alt])) { + $matches[$alt] = null; + } + } + } else { + foreach ($part['alternatives'] as $alt) { + if ($alt === $matchedName && !isset($matches[$alt])) { + $matches[$alt] = isset($this->defaults[$alt])? $this->defaults[$alt] : true; + } elseif (!isset($matches[$alt])) { + $matches[$alt] = false; + } + } + } + } + } + + /* + * Scan for left-out flags that should result in a mismatch + */ + foreach ($params as $param) { + if (preg_match('#^\-+#', $param)) { + return null; // there is an unrecognized flag + } + } + + /* + * Go through all positional params + */ + $argPos = 0; + foreach ($positional as &$part) { + /* + * Check if param exists + */ + if (!isset($params[$argPos])) { + if ($part['required']) { + // cannot find required positional param + return null; + } else { + // stop matching + break; + } + } + + $value = $params[$argPos]; + + /* + * Check if literal param matches + */ + if ($part['literal']) { + if ( + (isset($part['alternatives']) && !in_array($value, $part['alternatives'])) || + (!isset($part['alternatives']) && $value != $part['name']) + ) { + return null; + } + } + + /* + * Validate the value against constraints + */ + if ($part['hasValue'] && isset($this->constraints[$part['name']])) { + if ( + !preg_match($this->constraints[$part['name']], $value) + ) { + // constraint failed + return null; + } + } + + /* + * Store the value + */ + if ($part['hasValue']) { + $matches[$part['name']] = $value; + } elseif (isset($part['alternatives'])) { + // from all alternatives set matching parameter to TRUE and the rest to FALSE + foreach ($part['alternatives'] as $alt) { + if ($alt == $value) { + $matches[$alt] = isset($this->defaults[$alt])? $this->defaults[$alt] : true; + } else { + $matches[$alt] = false; + } + } + + // set alternatives group value + $matches[$part['name']] = $value; + } elseif (!$part['required']) { + // set optional parameter flag + $name = $part['name']; + $matches[$name] = isset($this->defaults[$name])? $this->defaults[$name] : true; + } + + /* + * Advance to next argument + */ + $argPos++; + } + + /* + * Check if we have consumed all positional parameters + */ + if ($argPos < count($params)) { + return null; // there are extraneous params that were not consumed + } + + /* + * Any optional flags that were not entered have value false + */ + foreach ($this->parts as &$part) { + if (!$part['required'] && !$part['hasValue']) { + if (!isset($matches[$part['name']])) { + $matches[$part['name']] = false; + } + // unset alternatives also should be false + if (isset($part['alternatives'])) { + foreach ($part['alternatives'] as $alt) { + if (!isset($matches[$alt])) { + $matches[$alt] = false; + } + } + } + } + } + + // run filters + foreach ($matches as $name => $value) { + if (isset($this->filters[$name])) { + $matches[$name] = $this->filters[$name]->filter($value); + } + } + + // run validators + $valid = true; + foreach ($matches as $name => $value) { + if (isset($this->validators[$name])) { + $valid &= $this->validators[$name]->isValid($value); + } + } + + if (!$valid) { + return null; + } + + return array_replace($this->defaults, $matches); + } +} diff --git a/library/Zend/Console/RouteMatcher/RouteMatcherInterface.php b/library/Zend/Console/RouteMatcher/RouteMatcherInterface.php new file mode 100755 index 0000000000..62f463c335 --- /dev/null +++ b/library/Zend/Console/RouteMatcher/RouteMatcherInterface.php @@ -0,0 +1,21 @@ +=5.3.23", + "zendframework/zend-stdlib": "self.version" + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev", + "dev-develop": "2.4-dev" + } + } +} diff --git a/library/Zend/Crypt/BlockCipher.php b/library/Zend/Crypt/BlockCipher.php new file mode 100755 index 0000000000..9b2a713ecd --- /dev/null +++ b/library/Zend/Crypt/BlockCipher.php @@ -0,0 +1,489 @@ +cipher = $cipher; + } + + /** + * Factory. + * + * @param string $adapter + * @param array $options + * @return BlockCipher + */ + public static function factory($adapter, $options = array()) + { + $plugins = static::getSymmetricPluginManager(); + $adapter = $plugins->get($adapter, (array) $options); + + return new static($adapter); + } + + /** + * Returns the symmetric cipher plugin manager. If it doesn't exist it's created. + * + * @return SymmetricPluginManager + */ + public static function getSymmetricPluginManager() + { + if (static::$symmetricPlugins === null) { + static::setSymmetricPluginManager(new SymmetricPluginManager()); + } + + return static::$symmetricPlugins; + } + + /** + * Set the symmetric cipher plugin manager + * + * @param string|SymmetricPluginManager $plugins + * @throws Exception\InvalidArgumentException + */ + public static function setSymmetricPluginManager($plugins) + { + if (is_string($plugins)) { + if (!class_exists($plugins)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Unable to locate symmetric cipher plugins using class "%s"; class does not exist', + $plugins + )); + } + $plugins = new $plugins(); + } + if (!$plugins instanceof SymmetricPluginManager) { + throw new Exception\InvalidArgumentException(sprintf( + 'Expected an instance or extension of %s\SymmetricPluginManager; received "%s"', + __NAMESPACE__, + (is_object($plugins) ? get_class($plugins) : gettype($plugins)) + )); + } + static::$symmetricPlugins = $plugins; + } + + /** + * Set the symmetric cipher + * + * @param SymmetricInterface $cipher + * @return BlockCipher + */ + public function setCipher(SymmetricInterface $cipher) + { + $this->cipher = $cipher; + return $this; + } + + /** + * Get symmetric cipher + * + * @return SymmetricInterface + */ + public function getCipher() + { + return $this->cipher; + } + + /** + * Set the number of iterations for Pbkdf2 + * + * @param int $num + * @return BlockCipher + */ + public function setKeyIteration($num) + { + $this->keyIteration = (int) $num; + + return $this; + } + + /** + * Get the number of iterations for Pbkdf2 + * + * @return int + */ + public function getKeyIteration() + { + return $this->keyIteration; + } + + /** + * Set the salt (IV) + * + * @param string $salt + * @return BlockCipher + * @throws Exception\InvalidArgumentException + */ + public function setSalt($salt) + { + try { + $this->cipher->setSalt($salt); + } catch (Symmetric\Exception\InvalidArgumentException $e) { + throw new Exception\InvalidArgumentException("The salt is not valid: " . $e->getMessage()); + } + $this->saltSetted = true; + + return $this; + } + + /** + * Get the salt (IV) according to the size requested by the algorithm + * + * @return string + */ + public function getSalt() + { + return $this->cipher->getSalt(); + } + + /** + * Get the original salt value + * + * @return string + */ + public function getOriginalSalt() + { + return $this->cipher->getOriginalSalt(); + } + + /** + * Enable/disable the binary output + * + * @param bool $value + * @return BlockCipher + */ + public function setBinaryOutput($value) + { + $this->binaryOutput = (bool) $value; + + return $this; + } + + /** + * Get the value of binary output + * + * @return bool + */ + public function getBinaryOutput() + { + return $this->binaryOutput; + } + + /** + * Set the encryption/decryption key + * + * @param string $key + * @return BlockCipher + * @throws Exception\InvalidArgumentException + */ + public function setKey($key) + { + if (empty($key)) { + throw new Exception\InvalidArgumentException('The key cannot be empty'); + } + $this->key = $key; + + return $this; + } + + /** + * Get the key + * + * @return string + */ + public function getKey() + { + return $this->key; + } + + /** + * Set algorithm of the symmetric cipher + * + * @param string $algo + * @return BlockCipher + * @throws Exception\InvalidArgumentException + */ + public function setCipherAlgorithm($algo) + { + if (empty($this->cipher)) { + throw new Exception\InvalidArgumentException('No symmetric cipher specified'); + } + try { + $this->cipher->setAlgorithm($algo); + } catch (Symmetric\Exception\InvalidArgumentException $e) { + throw new Exception\InvalidArgumentException($e->getMessage()); + } + + return $this; + } + + /** + * Get the cipher algorithm + * + * @return string|bool + */ + public function getCipherAlgorithm() + { + if (!empty($this->cipher)) { + return $this->cipher->getAlgorithm(); + } + + return false; + } + + /** + * Get the supported algorithms of the symmetric cipher + * + * @return array + */ + public function getCipherSupportedAlgorithms() + { + if (!empty($this->cipher)) { + return $this->cipher->getSupportedAlgorithms(); + } + + return array(); + } + + /** + * Set the hash algorithm for HMAC authentication + * + * @param string $hash + * @return BlockCipher + * @throws Exception\InvalidArgumentException + */ + public function setHashAlgorithm($hash) + { + if (!Hash::isSupported($hash)) { + throw new Exception\InvalidArgumentException( + "The specified hash algorithm '{$hash}' is not supported by Zend\Crypt\Hash" + ); + } + $this->hash = $hash; + + return $this; + } + + /** + * Get the hash algorithm for HMAC authentication + * + * @return string + */ + public function getHashAlgorithm() + { + return $this->hash; + } + + /** + * Set the hash algorithm for the Pbkdf2 + * + * @param string $hash + * @return BlockCipher + * @throws Exception\InvalidArgumentException + */ + public function setPbkdf2HashAlgorithm($hash) + { + if (!Hash::isSupported($hash)) { + throw new Exception\InvalidArgumentException( + "The specified hash algorithm '{$hash}' is not supported by Zend\Crypt\Hash" + ); + } + $this->pbkdf2Hash = $hash; + + return $this; + } + + /** + * Get the Pbkdf2 hash algorithm + * + * @return string + */ + public function getPbkdf2HashAlgorithm() + { + return $this->pbkdf2Hash; + } + + /** + * Encrypt then authenticate using HMAC + * + * @param string $data + * @return string + * @throws Exception\InvalidArgumentException + */ + public function encrypt($data) + { + // 0 (as integer), 0.0 (as float) & '0' (as string) will return false, though these should be allowed + // Must be a string, integer, or float in order to encrypt + if ((is_string($data) && $data === '') + || is_array($data) + || is_object($data) + ) { + throw new Exception\InvalidArgumentException('The data to encrypt cannot be empty'); + } + + // Cast to string prior to encrypting + if (!is_string($data)) { + $data = (string) $data; + } + + if (empty($this->cipher)) { + throw new Exception\InvalidArgumentException('No symmetric cipher specified'); + } + if (empty($this->key)) { + throw new Exception\InvalidArgumentException('No key specified for the encryption'); + } + $keySize = $this->cipher->getKeySize(); + // generate a random salt (IV) if the salt has not been set + if (!$this->saltSetted) { + $this->cipher->setSalt(Rand::getBytes($this->cipher->getSaltSize(), true)); + } + // generate the encryption key and the HMAC key for the authentication + $hash = Pbkdf2::calc( + $this->getPbkdf2HashAlgorithm(), + $this->getKey(), + $this->getSalt(), + $this->keyIteration, + $keySize * 2 + ); + // set the encryption key + $this->cipher->setKey(substr($hash, 0, $keySize)); + // set the key for HMAC + $keyHmac = substr($hash, $keySize); + // encryption + $ciphertext = $this->cipher->encrypt($data); + // HMAC + $hmac = Hmac::compute($keyHmac, $this->hash, $this->cipher->getAlgorithm() . $ciphertext); + if (!$this->binaryOutput) { + $ciphertext = base64_encode($ciphertext); + } + + return $hmac . $ciphertext; + } + + /** + * Decrypt + * + * @param string $data + * @return string|bool + * @throws Exception\InvalidArgumentException + */ + public function decrypt($data) + { + if (!is_string($data)) { + throw new Exception\InvalidArgumentException('The data to decrypt must be a string'); + } + if ('' === $data) { + throw new Exception\InvalidArgumentException('The data to decrypt cannot be empty'); + } + if (empty($this->key)) { + throw new Exception\InvalidArgumentException('No key specified for the decryption'); + } + if (empty($this->cipher)) { + throw new Exception\InvalidArgumentException('No symmetric cipher specified'); + } + $hmacSize = Hmac::getOutputSize($this->hash); + $hmac = substr($data, 0, $hmacSize); + $ciphertext = substr($data, $hmacSize); + if (!$this->binaryOutput) { + $ciphertext = base64_decode($ciphertext); + } + $iv = substr($ciphertext, 0, $this->cipher->getSaltSize()); + $keySize = $this->cipher->getKeySize(); + // generate the encryption key and the HMAC key for the authentication + $hash = Pbkdf2::calc( + $this->getPbkdf2HashAlgorithm(), + $this->getKey(), + $iv, + $this->keyIteration, + $keySize * 2 + ); + // set the decryption key + $this->cipher->setKey(substr($hash, 0, $keySize)); + // set the key for HMAC + $keyHmac = substr($hash, $keySize); + $hmacNew = Hmac::compute($keyHmac, $this->hash, $this->cipher->getAlgorithm() . $ciphertext); + if (!Utils::compareStrings($hmacNew, $hmac)) { + return false; + } + + return $this->cipher->decrypt($ciphertext); + } +} diff --git a/library/Zend/Crypt/CONTRIBUTING.md b/library/Zend/Crypt/CONTRIBUTING.md new file mode 100755 index 0000000000..e77f5d2d5b --- /dev/null +++ b/library/Zend/Crypt/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# CONTRIBUTING + +Please don't open pull requests against this repository, please use https://github.com/zendframework/zf2. \ No newline at end of file diff --git a/library/Zend/Crypt/Exception/ExceptionInterface.php b/library/Zend/Crypt/Exception/ExceptionInterface.php new file mode 100755 index 0000000000..ef93743614 --- /dev/null +++ b/library/Zend/Crypt/Exception/ExceptionInterface.php @@ -0,0 +1,14 @@ + MHASH_MD2, + 'md4' => MHASH_MD4, + 'md5' => MHASH_MD5, + 'sha1' => MHASH_SHA1, + 'sha224' => MHASH_SHA224, + 'sha256' => MHASH_SHA256, + 'sha384' => MHASH_SHA384, + 'sha512' => MHASH_SHA512, + 'ripemd128' => MHASH_RIPEMD128, + 'ripemd256' => MHASH_RIPEMD256, + 'ripemd320' => MHASH_RIPEMD320, + 'haval128,3' => MHASH_HAVAL128, + 'haval160,3' => MHASH_HAVAL160, + 'haval192,3' => MHASH_HAVAL192, + 'haval224,3' => MHASH_HAVAL224, + 'haval256,3' => MHASH_HAVAL256, + 'tiger128,3' => MHASH_TIGER128, + 'riger160,3' => MHASH_TIGER160, + 'whirpool' => MHASH_WHIRLPOOL, + 'snefru256' => MHASH_SNEFRU256, + 'gost' => MHASH_GOST, + 'crc32' => MHASH_CRC32, + 'crc32b' => MHASH_CRC32B + ); + + /** + * Generate the new key + * + * @param string $hash The hash algorithm to be used by HMAC + * @param string $password The source password/key + * @param int $bytes The output size in bytes + * @param string $salt The salt of the algorithm + * @throws Exception\InvalidArgumentException + * @return string + */ + public static function calc($hash, $password, $salt, $bytes) + { + if (!in_array($hash, array_keys(static::$supportedMhashAlgos))) { + throw new Exception\InvalidArgumentException("The hash algorithm $hash is not supported by " . __CLASS__); + } + if (strlen($salt)<8) { + throw new Exception\InvalidArgumentException('The salt size must be at least of 8 bytes'); + } + return mhash_keygen_s2k(static::$supportedMhashAlgos[$hash], $password, $salt, $bytes); + } +} diff --git a/library/Zend/Crypt/Key/Derivation/Scrypt.php b/library/Zend/Crypt/Key/Derivation/Scrypt.php new file mode 100755 index 0000000000..da2a783677 --- /dev/null +++ b/library/Zend/Crypt/Key/Derivation/Scrypt.php @@ -0,0 +1,340 @@ + 0 and a power of 2"); + } + if ($n > PHP_INT_MAX / 128 / $r) { + throw new Exception\InvalidArgumentException("Parameter n is too large"); + } + if ($r > PHP_INT_MAX / 128 / $p) { + throw new Exception\InvalidArgumentException("Parameter r is too large"); + } + + if (extension_loaded('Scrypt')) { + if ($length < 16) { + throw new Exception\InvalidArgumentException("Key length is too low, must be greater or equal to 16"); + } + return self::hex2bin(scrypt($password, $salt, $n, $r, $p, $length)); + } + + $b = Pbkdf2::calc('sha256', $password, $salt, 1, $p * 128 * $r); + + $s = ''; + for ($i = 0; $i < $p; $i++) { + $s .= self::scryptROMix(substr($b, $i * 128 * $r, 128 * $r), $n, $r); + } + + return Pbkdf2::calc('sha256', $password, $s, 1, $length); + } + + /** + * scryptROMix + * + * @param string $b + * @param int $n + * @param int $r + * @return string + * @see https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#section-4 + */ + protected static function scryptROMix($b, $n, $r) + { + $x = $b; + $v = array(); + for ($i = 0; $i < $n; $i++) { + $v[$i] = $x; + $x = self::scryptBlockMix($x, $r); + } + for ($i = 0; $i < $n; $i++) { + $j = self::integerify($x) % $n; + $t = $x ^ $v[$j]; + $x = self::scryptBlockMix($t, $r); + } + return $x; + } + + /** + * scryptBlockMix + * + * @param string $b + * @param int $r + * @return string + * @see https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#section-3 + */ + protected static function scryptBlockMix($b, $r) + { + $x = substr($b, -64); + $even = ''; + $odd = ''; + $len = 2 * $r; + + for ($i = 0; $i < $len; $i++) { + if (PHP_INT_SIZE === 4) { + $x = self::salsa208Core32($x ^ substr($b, 64 * $i, 64)); + } else { + $x = self::salsa208Core64($x ^ substr($b, 64 * $i, 64)); + } + if ($i % 2 == 0) { + $even .= $x; + } else { + $odd .= $x; + } + } + return $even . $odd; + } + + /** + * Salsa 20/8 core (32 bit version) + * + * @param string $b + * @return string + * @see https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#section-2 + * @see http://cr.yp.to/salsa20.html + */ + protected static function salsa208Core32($b) + { + $b32 = array(); + for ($i = 0; $i < 16; $i++) { + list(, $b32[$i]) = unpack("V", substr($b, $i * 4, 4)); + } + + $x = $b32; + for ($i = 0; $i < 8; $i += 2) { + $a = ($x[ 0] + $x[12]); + $x[ 4] ^= ($a << 7) | ($a >> 25) & 0x7f; + $a = ($x[ 4] + $x[ 0]); + $x[ 8] ^= ($a << 9) | ($a >> 23) & 0x1ff; + $a = ($x[ 8] + $x[ 4]); + $x[12] ^= ($a << 13) | ($a >> 19) & 0x1fff; + $a = ($x[12] + $x[ 8]); + $x[ 0] ^= ($a << 18) | ($a >> 14) & 0x3ffff; + $a = ($x[ 5] + $x[ 1]); + $x[ 9] ^= ($a << 7) | ($a >> 25) & 0x7f; + $a = ($x[ 9] + $x[ 5]); + $x[13] ^= ($a << 9) | ($a >> 23) & 0x1ff; + $a = ($x[13] + $x[ 9]); + $x[ 1] ^= ($a << 13) | ($a >> 19) & 0x1fff; + $a = ($x[ 1] + $x[13]); + $x[ 5] ^= ($a << 18) | ($a >> 14) & 0x3ffff; + $a = ($x[10] + $x[ 6]); + $x[14] ^= ($a << 7) | ($a >> 25) & 0x7f; + $a = ($x[14] + $x[10]); + $x[ 2] ^= ($a << 9) | ($a >> 23) & 0x1ff; + $a = ($x[ 2] + $x[14]); + $x[ 6] ^= ($a << 13) | ($a >> 19) & 0x1fff; + $a = ($x[ 6] + $x[ 2]); + $x[10] ^= ($a << 18) | ($a >> 14) & 0x3ffff; + $a = ($x[15] + $x[11]); + $x[ 3] ^= ($a << 7) | ($a >> 25) & 0x7f; + $a = ($x[ 3] + $x[15]); + $x[ 7] ^= ($a << 9) | ($a >> 23) & 0x1ff; + $a = ($x[ 7] + $x[ 3]); + $x[11] ^= ($a << 13) | ($a >> 19) & 0x1fff; + $a = ($x[11] + $x[ 7]); + $x[15] ^= ($a << 18) | ($a >> 14) & 0x3ffff; + $a = ($x[ 0] + $x[ 3]); + $x[ 1] ^= ($a << 7) | ($a >> 25) & 0x7f; + $a = ($x[ 1] + $x[ 0]); + $x[ 2] ^= ($a << 9) | ($a >> 23) & 0x1ff; + $a = ($x[ 2] + $x[ 1]); + $x[ 3] ^= ($a << 13) | ($a >> 19) & 0x1fff; + $a = ($x[ 3] + $x[ 2]); + $x[ 0] ^= ($a << 18) | ($a >> 14) & 0x3ffff; + $a = ($x[ 5] + $x[ 4]); + $x[ 6] ^= ($a << 7) | ($a >> 25) & 0x7f; + $a = ($x[ 6] + $x[ 5]); + $x[ 7] ^= ($a << 9) | ($a >> 23) & 0x1ff; + $a = ($x[ 7] + $x[ 6]); + $x[ 4] ^= ($a << 13) | ($a >> 19) & 0x1fff; + $a = ($x[ 4] + $x[ 7]); + $x[ 5] ^= ($a << 18) | ($a >> 14) & 0x3ffff; + $a = ($x[10] + $x[ 9]); + $x[11] ^= ($a << 7) | ($a >> 25) & 0x7f; + $a = ($x[11] + $x[10]); + $x[ 8] ^= ($a << 9) | ($a >> 23) & 0x1ff; + $a = ($x[ 8] + $x[11]); + $x[ 9] ^= ($a << 13) | ($a >> 19) & 0x1fff; + $a = ($x[ 9] + $x[ 8]); + $x[10] ^= ($a << 18) | ($a >> 14) & 0x3ffff; + $a = ($x[15] + $x[14]); + $x[12] ^= ($a << 7) | ($a >> 25) & 0x7f; + $a = ($x[12] + $x[15]); + $x[13] ^= ($a << 9) | ($a >> 23) & 0x1ff; + $a = ($x[13] + $x[12]); + $x[14] ^= ($a << 13) | ($a >> 19) & 0x1fff; + $a = ($x[14] + $x[13]); + $x[15] ^= ($a << 18) | ($a >> 14) & 0x3ffff; + } + for ($i = 0; $i < 16; $i++) { + $b32[$i] = $b32[$i] + $x[$i]; + } + $result = ''; + for ($i = 0; $i < 16; $i++) { + $result .= pack("V", $b32[$i]); + } + + return $result; + } + + /** + * Salsa 20/8 core (64 bit version) + * + * @param string $b + * @return string + * @see https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#section-2 + * @see http://cr.yp.to/salsa20.html + */ + protected static function salsa208Core64($b) + { + $b32 = array(); + for ($i = 0; $i < 16; $i++) { + list(, $b32[$i]) = unpack("V", substr($b, $i * 4, 4)); + } + + $x = $b32; + for ($i = 0; $i < 8; $i += 2) { + $a = ($x[ 0] + $x[12]) & 0xffffffff; + $x[ 4] ^= ($a << 7) | ($a >> 25); + $a = ($x[ 4] + $x[ 0]) & 0xffffffff; + $x[ 8] ^= ($a << 9) | ($a >> 23); + $a = ($x[ 8] + $x[ 4]) & 0xffffffff; + $x[12] ^= ($a << 13) | ($a >> 19); + $a = ($x[12] + $x[ 8]) & 0xffffffff; + $x[ 0] ^= ($a << 18) | ($a >> 14); + $a = ($x[ 5] + $x[ 1]) & 0xffffffff; + $x[ 9] ^= ($a << 7) | ($a >> 25); + $a = ($x[ 9] + $x[ 5]) & 0xffffffff; + $x[13] ^= ($a << 9) | ($a >> 23); + $a = ($x[13] + $x[ 9]) & 0xffffffff; + $x[ 1] ^= ($a << 13) | ($a >> 19); + $a = ($x[ 1] + $x[13]) & 0xffffffff; + $x[ 5] ^= ($a << 18) | ($a >> 14); + $a = ($x[10] + $x[ 6]) & 0xffffffff; + $x[14] ^= ($a << 7) | ($a >> 25); + $a = ($x[14] + $x[10]) & 0xffffffff; + $x[ 2] ^= ($a << 9) | ($a >> 23); + $a = ($x[ 2] + $x[14]) & 0xffffffff; + $x[ 6] ^= ($a << 13) | ($a >> 19); + $a = ($x[ 6] + $x[ 2]) & 0xffffffff; + $x[10] ^= ($a << 18) | ($a >> 14); + $a = ($x[15] + $x[11]) & 0xffffffff; + $x[ 3] ^= ($a << 7) | ($a >> 25); + $a = ($x[ 3] + $x[15]) & 0xffffffff; + $x[ 7] ^= ($a << 9) | ($a >> 23); + $a = ($x[ 7] + $x[ 3]) & 0xffffffff; + $x[11] ^= ($a << 13) | ($a >> 19); + $a = ($x[11] + $x[ 7]) & 0xffffffff; + $x[15] ^= ($a << 18) | ($a >> 14); + $a = ($x[ 0] + $x[ 3]) & 0xffffffff; + $x[ 1] ^= ($a << 7) | ($a >> 25); + $a = ($x[ 1] + $x[ 0]) & 0xffffffff; + $x[ 2] ^= ($a << 9) | ($a >> 23); + $a = ($x[ 2] + $x[ 1]) & 0xffffffff; + $x[ 3] ^= ($a << 13) | ($a >> 19); + $a = ($x[ 3] + $x[ 2]) & 0xffffffff; + $x[ 0] ^= ($a << 18) | ($a >> 14); + $a = ($x[ 5] + $x[ 4]) & 0xffffffff; + $x[ 6] ^= ($a << 7) | ($a >> 25); + $a = ($x[ 6] + $x[ 5]) & 0xffffffff; + $x[ 7] ^= ($a << 9) | ($a >> 23); + $a = ($x[ 7] + $x[ 6]) & 0xffffffff; + $x[ 4] ^= ($a << 13) | ($a >> 19); + $a = ($x[ 4] + $x[ 7]) & 0xffffffff; + $x[ 5] ^= ($a << 18) | ($a >> 14); + $a = ($x[10] + $x[ 9]) & 0xffffffff; + $x[11] ^= ($a << 7) | ($a >> 25); + $a = ($x[11] + $x[10]) & 0xffffffff; + $x[ 8] ^= ($a << 9) | ($a >> 23); + $a = ($x[ 8] + $x[11]) & 0xffffffff; + $x[ 9] ^= ($a << 13) | ($a >> 19); + $a = ($x[ 9] + $x[ 8]) & 0xffffffff; + $x[10] ^= ($a << 18) | ($a >> 14); + $a = ($x[15] + $x[14]) & 0xffffffff; + $x[12] ^= ($a << 7) | ($a >> 25); + $a = ($x[12] + $x[15]) & 0xffffffff; + $x[13] ^= ($a << 9) | ($a >> 23); + $a = ($x[13] + $x[12]) & 0xffffffff; + $x[14] ^= ($a << 13) | ($a >> 19); + $a = ($x[14] + $x[13]) & 0xffffffff; + $x[15] ^= ($a << 18) | ($a >> 14); + } + for ($i = 0; $i < 16; $i++) { + $b32[$i] = ($b32[$i] + $x[$i]) & 0xffffffff; + } + $result = ''; + for ($i = 0; $i < 16; $i++) { + $result .= pack("V", $b32[$i]); + } + + return $result; + } + + /** + * Integerify + * + * Integerify (B[0] ... B[2 * r - 1]) is defined as the result + * of interpreting B[2 * r - 1] as a little-endian integer. + * Each block B is a string of 64 bytes. + * + * @param string $b + * @return int + * @see https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#section-4 + */ + protected static function integerify($b) + { + $v = 'v'; + if (PHP_INT_SIZE === 8) { + $v = 'V'; + } + list(,$n) = unpack($v, substr($b, -64)); + return $n; + } + + /** + * Convert hex string in a binary string + * + * @param string $hex + * @return string + */ + protected static function hex2bin($hex) + { + if (PHP_VERSION_ID >= 50400) { + return hex2bin($hex); + } + $len = strlen($hex); + $result = ''; + for ($i = 0; $i < $len; $i+=2) { + $result .= chr(hexdec($hex[$i] . $hex[$i+1])); + } + return $result; + } +} diff --git a/library/Zend/Crypt/Password/Apache.php b/library/Zend/Crypt/Password/Apache.php new file mode 100755 index 0000000000..d9359827a1 --- /dev/null +++ b/library/Zend/Crypt/Password/Apache.php @@ -0,0 +1,301 @@ + $value) { + switch (strtolower($key)) { + case 'format': + $this->setFormat($value); + break; + case 'authname': + $this->setAuthName($value); + break; + case 'username': + $this->setUserName($value); + break; + } + } + } + + /** + * Generate the hash of a password + * + * @param string $password + * @throws Exception\RuntimeException + * @return string + */ + public function create($password) + { + if (empty($this->format)) { + throw new Exception\RuntimeException( + 'You must specify a password format' + ); + } + switch ($this->format) { + case 'crypt': + $hash = crypt($password, Rand::getString(2, self::ALPHA64)); + break; + case 'sha1': + $hash = '{SHA}' . base64_encode(sha1($password, true)); + break; + case 'md5': + $hash = $this->apr1Md5($password); + break; + case 'digest': + if (empty($this->userName) || empty($this->authName)) { + throw new Exception\RuntimeException( + 'You must specify UserName and AuthName (realm) to generate the digest' + ); + } + $hash = md5($this->userName . ':' . $this->authName . ':' .$password); + break; + } + + return $hash; + } + + /** + * Verify if a password is correct against a hash value + * + * @param string $password + * @param string $hash + * @return bool + */ + public function verify($password, $hash) + { + if (substr($hash, 0, 5) === '{SHA}') { + $hash2 = '{SHA}' . base64_encode(sha1($password, true)); + return ($hash === $hash2); + } + if (substr($hash, 0, 6) === '$apr1$') { + $token = explode('$', $hash); + if (empty($token[2])) { + throw new Exception\InvalidArgumentException( + 'The APR1 password format is not valid' + ); + } + $hash2 = $this->apr1Md5($password, $token[2]); + return ($hash === $hash2); + } + if (strlen($hash) > 13) { // digest + if (empty($this->userName) || empty($this->authName)) { + throw new Exception\RuntimeException( + 'You must specify UserName and AuthName (realm) to verify the digest' + ); + } + $hash2 = md5($this->userName . ':' . $this->authName . ':' .$password); + return ($hash === $hash2); + } + return (crypt($password, $hash) === $hash); + } + + /** + * Set the format of the password + * + * @param string $format + * @throws Exception\InvalidArgumentException + * @return Apache + */ + public function setFormat($format) + { + $format = strtolower($format); + if (!in_array($format, $this->supportedFormat)) { + throw new Exception\InvalidArgumentException(sprintf( + 'The format %s specified is not valid. The supported formats are: %s', + $format, + implode(',', $this->supportedFormat) + )); + } + $this->format = $format; + + return $this; + } + + /** + * Get the format of the password + * + * @return string + */ + public function getFormat() + { + return $this->format; + } + + /** + * Set the AuthName (for digest authentication) + * + * @param string $name + * @return Apache + */ + public function setAuthName($name) + { + $this->authName = $name; + + return $this; + } + + /** + * Get the AuthName (for digest authentication) + * + * @return string + */ + public function getAuthName() + { + return $this->authName; + } + + /** + * Set the username + * + * @param string $name + * @return Apache + */ + public function setUserName($name) + { + $this->userName = $name; + + return $this; + } + + /** + * Get the username + * + * @return string + */ + public function getUserName() + { + return $this->userName; + } + + /** + * Convert a binary string using the alphabet "./0-9A-Za-z" + * + * @param string $value + * @return string + */ + protected function toAlphabet64($value) + { + return strtr(strrev(substr(base64_encode($value), 2)), self::BASE64, self::ALPHA64); + } + + /** + * APR1 MD5 algorithm + * + * @param string $password + * @param null|string $salt + * @return string + */ + protected function apr1Md5($password, $salt = null) + { + if (null === $salt) { + $salt = Rand::getString(8, self::ALPHA64); + } else { + if (strlen($salt) !== 8) { + throw new Exception\InvalidArgumentException( + 'The salt value for APR1 algorithm must be 8 characters long' + ); + } + for ($i = 0; $i < 8; $i++) { + if (strpos(self::ALPHA64, $salt[$i]) === false) { + throw new Exception\InvalidArgumentException( + 'The salt value must be a string in the alphabet "./0-9A-Za-z"' + ); + } + } + } + $len = strlen($password); + $text = $password . '$apr1$' . $salt; + $bin = pack("H32", md5($password . $salt . $password)); + for ($i = $len; $i > 0; $i -= 16) { + $text .= substr($bin, 0, min(16, $i)); + } + for ($i = $len; $i > 0; $i >>= 1) { + $text .= ($i & 1) ? chr(0) : $password[0]; + } + $bin = pack("H32", md5($text)); + for ($i = 0; $i < 1000; $i++) { + $new = ($i & 1) ? $password : $bin; + if ($i % 3) { + $new .= $salt; + } + if ($i % 7) { + $new .= $password; + } + $new .= ($i & 1) ? $bin : $password; + $bin = pack("H32", md5($new)); + } + $tmp = ''; + for ($i = 0; $i < 5; $i++) { + $k = $i + 6; + $j = $i + 12; + if ($j == 16) { + $j = 5; + } + $tmp = $bin[$i] . $bin[$k] . $bin[$j] . $tmp; + } + $tmp = chr(0) . chr(0) . $bin[11] . $tmp; + + return '$apr1$' . $salt . '$' . $this->toAlphabet64($tmp); + } +} diff --git a/library/Zend/Crypt/Password/Bcrypt.php b/library/Zend/Crypt/Password/Bcrypt.php new file mode 100755 index 0000000000..b489ddd48f --- /dev/null +++ b/library/Zend/Crypt/Password/Bcrypt.php @@ -0,0 +1,207 @@ + $value) { + switch (strtolower($key)) { + case 'salt': + $this->setSalt($value); + break; + case 'cost': + $this->setCost($value); + break; + } + } + } + } + + /** + * Bcrypt + * + * @param string $password + * @throws Exception\RuntimeException + * @return string + */ + public function create($password) + { + if (empty($this->salt)) { + $salt = Rand::getBytes(self::MIN_SALT_SIZE); + } else { + $salt = $this->salt; + } + $salt64 = substr(str_replace('+', '.', base64_encode($salt)), 0, 22); + /** + * Check for security flaw in the bcrypt implementation used by crypt() + * @see http://php.net/security/crypt_blowfish.php + */ + if ((PHP_VERSION_ID >= 50307) && !$this->backwardCompatibility) { + $prefix = '$2y$'; + } else { + $prefix = '$2a$'; + // check if the password contains 8-bit character + if (preg_match('/[\x80-\xFF]/', $password)) { + throw new Exception\RuntimeException( + 'The bcrypt implementation used by PHP can contain a security flaw ' . + 'using password with 8-bit character. ' . + 'We suggest to upgrade to PHP 5.3.7+ or use passwords with only 7-bit characters' + ); + } + } + $hash = crypt($password, $prefix . $this->cost . '$' . $salt64); + if (strlen($hash) < 13) { + throw new Exception\RuntimeException('Error during the bcrypt generation'); + } + return $hash; + } + + /** + * Verify if a password is correct against a hash value + * + * @param string $password + * @param string $hash + * @throws Exception\RuntimeException when the hash is unable to be processed + * @return bool + */ + public function verify($password, $hash) + { + $result = crypt($password, $hash); + if ($result === $hash) { + return true; + } + return false; + } + + /** + * Set the cost parameter + * + * @param int|string $cost + * @throws Exception\InvalidArgumentException + * @return Bcrypt + */ + public function setCost($cost) + { + if (!empty($cost)) { + $cost = (int) $cost; + if ($cost < 4 || $cost > 31) { + throw new Exception\InvalidArgumentException( + 'The cost parameter of bcrypt must be in range 04-31' + ); + } + $this->cost = sprintf('%1$02d', $cost); + } + return $this; + } + + /** + * Get the cost parameter + * + * @return string + */ + public function getCost() + { + return $this->cost; + } + + /** + * Set the salt value + * + * @param string $salt + * @throws Exception\InvalidArgumentException + * @return Bcrypt + */ + public function setSalt($salt) + { + if (strlen($salt) < self::MIN_SALT_SIZE) { + throw new Exception\InvalidArgumentException( + 'The length of the salt must be at least ' . self::MIN_SALT_SIZE . ' bytes' + ); + } + $this->salt = $salt; + return $this; + } + + /** + * Get the salt value + * + * @return string + */ + public function getSalt() + { + return $this->salt; + } + + /** + * Set the backward compatibility $2a$ instead of $2y$ for PHP 5.3.7+ + * + * @param bool $value + * @return Bcrypt + */ + public function setBackwardCompatibility($value) + { + $this->backwardCompatibility = (bool) $value; + return $this; + } + + /** + * Get the backward compatibility + * + * @return bool + */ + public function getBackwardCompatibility() + { + return $this->backwardCompatibility; + } +} diff --git a/library/Zend/Crypt/Password/Exception/ExceptionInterface.php b/library/Zend/Crypt/Password/Exception/ExceptionInterface.php new file mode 100755 index 0000000000..5c65fb7811 --- /dev/null +++ b/library/Zend/Crypt/Password/Exception/ExceptionInterface.php @@ -0,0 +1,16 @@ +setPrime($prime); + $this->setGenerator($generator); + if ($privateKey !== null) { + $this->setPrivateKey($privateKey, $privateKeyFormat); + } + + // set up BigInteger adapter + $this->math = Math\BigInteger\BigInteger::factory(); + } + + /** + * Set whether to use openssl extension + * + * @static + * @param bool $flag + */ + public static function useOpensslExtension($flag = true) + { + static::$useOpenssl = (bool) $flag; + } + + /** + * Generate own public key. If a private number has not already been set, + * one will be generated at this stage. + * + * @return DiffieHellman + * @throws \Zend\Crypt\Exception\RuntimeException + */ + public function generateKeys() + { + if (function_exists('openssl_dh_compute_key') && static::$useOpenssl !== false) { + $details = array( + 'p' => $this->convert($this->getPrime(), self::FORMAT_NUMBER, self::FORMAT_BINARY), + 'g' => $this->convert($this->getGenerator(), self::FORMAT_NUMBER, self::FORMAT_BINARY) + ); + if ($this->hasPrivateKey()) { + $details['priv_key'] = $this->convert( + $this->privateKey, + self::FORMAT_NUMBER, + self::FORMAT_BINARY + ); + $opensslKeyResource = openssl_pkey_new(array('dh' => $details)); + } else { + $opensslKeyResource = openssl_pkey_new(array( + 'dh' => $details, + 'private_key_bits' => self::DEFAULT_KEY_SIZE, + 'private_key_type' => OPENSSL_KEYTYPE_DH + )); + } + + if (false === $opensslKeyResource) { + throw new Exception\RuntimeException( + 'Can not generate new key; openssl ' . openssl_error_string() + ); + } + + $data = openssl_pkey_get_details($opensslKeyResource); + + $this->setPrivateKey($data['dh']['priv_key'], self::FORMAT_BINARY); + $this->setPublicKey($data['dh']['pub_key'], self::FORMAT_BINARY); + + $this->opensslKeyResource = $opensslKeyResource; + } else { + // Private key is lazy generated in the absence of ext/openssl + $publicKey = $this->math->powmod($this->getGenerator(), $this->getPrivateKey(), $this->getPrime()); + $this->setPublicKey($publicKey); + } + + return $this; + } + + /** + * Setter for the value of the public number + * + * @param string $number + * @param string $format + * @return DiffieHellman + * @throws \Zend\Crypt\Exception\InvalidArgumentException + */ + public function setPublicKey($number, $format = self::FORMAT_NUMBER) + { + $number = $this->convert($number, $format, self::FORMAT_NUMBER); + if (!preg_match('/^\d+$/', $number)) { + throw new Exception\InvalidArgumentException('Invalid parameter; not a positive natural number'); + } + $this->publicKey = (string) $number; + + return $this; + } + + /** + * Returns own public key for communication to the second party to this transaction + * + * @param string $format + * @return string + * @throws \Zend\Crypt\Exception\InvalidArgumentException + */ + public function getPublicKey($format = self::FORMAT_NUMBER) + { + if ($this->publicKey === null) { + throw new Exception\InvalidArgumentException( + 'A public key has not yet been generated using a prior call to generateKeys()' + ); + } + + return $this->convert($this->publicKey, self::FORMAT_NUMBER, $format); + } + + /** + * Compute the shared secret key based on the public key received from the + * the second party to this transaction. This should agree to the secret + * key the second party computes on our own public key. + * Once in agreement, the key is known to only to both parties. + * By default, the function expects the public key to be in binary form + * which is the typical format when being transmitted. + * + * If you need the binary form of the shared secret key, call + * getSharedSecretKey() with the optional parameter for Binary output. + * + * @param string $publicKey + * @param string $publicKeyFormat + * @param string $secretKeyFormat + * @return string + * @throws \Zend\Crypt\Exception\InvalidArgumentException + * @throws \Zend\Crypt\Exception\RuntimeException + */ + public function computeSecretKey( + $publicKey, + $publicKeyFormat = self::FORMAT_NUMBER, + $secretKeyFormat = self::FORMAT_NUMBER + ) { + if (function_exists('openssl_dh_compute_key') && static::$useOpenssl !== false) { + $publicKey = $this->convert($publicKey, $publicKeyFormat, self::FORMAT_BINARY); + $secretKey = openssl_dh_compute_key($publicKey, $this->opensslKeyResource); + if (false === $secretKey) { + throw new Exception\RuntimeException( + 'Can not compute key; openssl ' . openssl_error_string() + ); + } + $this->secretKey = $this->convert($secretKey, self::FORMAT_BINARY, self::FORMAT_NUMBER); + } else { + $publicKey = $this->convert($publicKey, $publicKeyFormat, self::FORMAT_NUMBER); + if (!preg_match('/^\d+$/', $publicKey)) { + throw new Exception\InvalidArgumentException( + 'Invalid parameter; not a positive natural number' + ); + } + $this->secretKey = $this->math->powmod($publicKey, $this->getPrivateKey(), $this->getPrime()); + } + + return $this->getSharedSecretKey($secretKeyFormat); + } + + /** + * Return the computed shared secret key from the DiffieHellman transaction + * + * @param string $format + * @return string + * @throws \Zend\Crypt\Exception\InvalidArgumentException + */ + public function getSharedSecretKey($format = self::FORMAT_NUMBER) + { + if (!isset($this->secretKey)) { + throw new Exception\InvalidArgumentException( + 'A secret key has not yet been computed; call computeSecretKey() first' + ); + } + + return $this->convert($this->secretKey, self::FORMAT_NUMBER, $format); + } + + /** + * Setter for the value of the prime number + * + * @param string $number + * @return DiffieHellman + * @throws \Zend\Crypt\Exception\InvalidArgumentException + */ + public function setPrime($number) + { + if (!preg_match('/^\d+$/', $number) || $number < 11) { + throw new Exception\InvalidArgumentException( + 'Invalid parameter; not a positive natural number or too small: ' . + 'should be a large natural number prime' + ); + } + $this->prime = (string) $number; + + return $this; + } + + /** + * Getter for the value of the prime number + * + * @param string $format + * @return string + * @throws \Zend\Crypt\Exception\InvalidArgumentException + */ + public function getPrime($format = self::FORMAT_NUMBER) + { + if (!isset($this->prime)) { + throw new Exception\InvalidArgumentException('No prime number has been set'); + } + + return $this->convert($this->prime, self::FORMAT_NUMBER, $format); + } + + /** + * Setter for the value of the generator number + * + * @param string $number + * @return DiffieHellman + * @throws \Zend\Crypt\Exception\InvalidArgumentException + */ + public function setGenerator($number) + { + if (!preg_match('/^\d+$/', $number) || $number < 2) { + throw new Exception\InvalidArgumentException( + 'Invalid parameter; not a positive natural number greater than 1' + ); + } + $this->generator = (string) $number; + + return $this; + } + + /** + * Getter for the value of the generator number + * + * @param string $format + * @return string + * @throws \Zend\Crypt\Exception\InvalidArgumentException + */ + public function getGenerator($format = self::FORMAT_NUMBER) + { + if (!isset($this->generator)) { + throw new Exception\InvalidArgumentException('No generator number has been set'); + } + + return $this->convert($this->generator, self::FORMAT_NUMBER, $format); + } + + /** + * Setter for the value of the private number + * + * @param string $number + * @param string $format + * @return DiffieHellman + * @throws \Zend\Crypt\Exception\InvalidArgumentException + */ + public function setPrivateKey($number, $format = self::FORMAT_NUMBER) + { + $number = $this->convert($number, $format, self::FORMAT_NUMBER); + if (!preg_match('/^\d+$/', $number)) { + throw new Exception\InvalidArgumentException('Invalid parameter; not a positive natural number'); + } + $this->privateKey = (string) $number; + + return $this; + } + + /** + * Getter for the value of the private number + * + * @param string $format + * @return string + */ + public function getPrivateKey($format = self::FORMAT_NUMBER) + { + if (!$this->hasPrivateKey()) { + $this->setPrivateKey($this->generatePrivateKey(), self::FORMAT_BINARY); + } + + return $this->convert($this->privateKey, self::FORMAT_NUMBER, $format); + } + + /** + * Check whether a private key currently exists. + * + * @return bool + */ + public function hasPrivateKey() + { + return isset($this->privateKey); + } + + /** + * Convert number between formats + * + * @param string $number + * @param string $inputFormat + * @param string $outputFormat + * @return string + */ + protected function convert($number, $inputFormat = self::FORMAT_NUMBER, $outputFormat = self::FORMAT_BINARY) + { + if ($inputFormat == $outputFormat) { + return $number; + } + + // convert to number + switch ($inputFormat) { + case self::FORMAT_BINARY: + case self::FORMAT_BTWOC: + $number = $this->math->binToInt($number); + break; + case self::FORMAT_NUMBER: + default: + // do nothing + break; + } + + // convert to output format + switch ($outputFormat) { + case self::FORMAT_BINARY: + return $this->math->intToBin($number); + break; + case self::FORMAT_BTWOC: + return $this->math->intToBin($number, true); + break; + case self::FORMAT_NUMBER: + default: + return $number; + break; + } + } + + /** + * In the event a private number/key has not been set by the user, + * or generated by ext/openssl, a best attempt will be made to + * generate a random key. Having a random number generator installed + * on linux/bsd is highly recommended! The alternative is not recommended + * for production unless without any other option. + * + * @return string + */ + protected function generatePrivateKey() + { + return Math\Rand::getBytes(strlen($this->getPrime()), true); + } +} diff --git a/library/Zend/Crypt/PublicKey/Rsa.php b/library/Zend/Crypt/PublicKey/Rsa.php new file mode 100755 index 0000000000..44faaa7d78 --- /dev/null +++ b/library/Zend/Crypt/PublicKey/Rsa.php @@ -0,0 +1,333 @@ +setPrivateKey($privateKey); + } + if ($publicKey instanceof Rsa\PublicKey) { + $options->setPublicKey($publicKey); + } + + return new Rsa($options); + } + + /** + * Class constructor + * + * @param RsaOptions $options + * @throws Rsa\Exception\RuntimeException + */ + public function __construct(RsaOptions $options = null) + { + if (!extension_loaded('openssl')) { + throw new Exception\RuntimeException( + 'Zend\Crypt\PublicKey\Rsa requires openssl extension to be loaded' + ); + } + + if ($options === null) { + $this->options = new RsaOptions(); + } else { + $this->options = $options; + } + } + + /** + * Set options + * + * @param RsaOptions $options + * @return Rsa + */ + public function setOptions(RsaOptions $options) + { + $this->options = $options; + return $this; + } + + /** + * Get options + * + * @return RsaOptions + */ + public function getOptions() + { + return $this->options; + } + + /** + * Return last openssl error(s) + * + * @return string + */ + public function getOpensslErrorString() + { + $message = ''; + while (false !== ($error = openssl_error_string())) { + $message .= $error . "\n"; + } + return trim($message); + } + + /** + * Sign with private key + * + * @param string $data + * @param Rsa\PrivateKey $privateKey + * @return string + * @throws Rsa\Exception\RuntimeException + */ + public function sign($data, Rsa\PrivateKey $privateKey = null) + { + $signature = ''; + if (null === $privateKey) { + $privateKey = $this->options->getPrivateKey(); + } + + $result = openssl_sign( + $data, + $signature, + $privateKey->getOpensslKeyResource(), + $this->options->getOpensslSignatureAlgorithm() + ); + if (false === $result) { + throw new Exception\RuntimeException( + 'Can not generate signature; openssl ' . $this->getOpensslErrorString() + ); + } + + if ($this->options->getBinaryOutput()) { + return $signature; + } + + return base64_encode($signature); + } + + /** + * Verify signature with public key + * + * $signature can be encoded in base64 or not. $mode sets how the input must be processed: + * - MODE_AUTO: Check if the $signature is encoded in base64. Not recommended for performance. + * - MODE_BASE64: Decode $signature using base64 algorithm. + * - MODE_RAW: $signature is not encoded. + * + * @param string $data + * @param string $signature + * @param null|Rsa\PublicKey $publicKey + * @param int $mode Input encoding + * @return bool + * @throws Rsa\Exception\RuntimeException + * @see Rsa::MODE_AUTO + * @see Rsa::MODE_BASE64 + * @see Rsa::MODE_RAW + */ + public function verify( + $data, + $signature, + Rsa\PublicKey $publicKey = null, + $mode = self::MODE_AUTO + ) { + if (null === $publicKey) { + $publicKey = $this->options->getPublicKey(); + } + + switch ($mode) { + case self::MODE_AUTO: + // check if data is encoded in Base64 + $output = base64_decode($signature, true); + if ((false !== $output) && ($signature === base64_encode($output))) { + $signature = $output; + } + break; + case self::MODE_BASE64: + $signature = base64_decode($signature); + break; + case self::MODE_RAW: + default: + break; + } + + $result = openssl_verify( + $data, + $signature, + $publicKey->getOpensslKeyResource(), + $this->options->getOpensslSignatureAlgorithm() + ); + if (-1 === $result) { + throw new Exception\RuntimeException( + 'Can not verify signature; openssl ' . $this->getOpensslErrorString() + ); + } + + return ($result === 1); + } + + /** + * Encrypt with private/public key + * + * @param string $data + * @param Rsa\AbstractKey $key + * @return string + * @throws Rsa\Exception\InvalidArgumentException + */ + public function encrypt($data, Rsa\AbstractKey $key = null) + { + if (null === $key) { + $key = $this->options->getPublicKey(); + } + + if (null === $key) { + throw new Exception\InvalidArgumentException('No key specified for the decryption'); + } + + $encrypted = $key->encrypt($data); + + if ($this->options->getBinaryOutput()) { + return $encrypted; + } + + return base64_encode($encrypted); + } + + /** + * Decrypt with private/public key + * + * $data can be encoded in base64 or not. $mode sets how the input must be processed: + * - MODE_AUTO: Check if the $signature is encoded in base64. Not recommended for performance. + * - MODE_BASE64: Decode $data using base64 algorithm. + * - MODE_RAW: $data is not encoded. + * + * @param string $data + * @param Rsa\AbstractKey $key + * @param int $mode Input encoding + * @return string + * @throws Rsa\Exception\InvalidArgumentException + * @see Rsa::MODE_AUTO + * @see Rsa::MODE_BASE64 + * @see Rsa::MODE_RAW + */ + public function decrypt( + $data, + Rsa\AbstractKey $key = null, + $mode = self::MODE_AUTO + ) { + if (null === $key) { + $key = $this->options->getPrivateKey(); + } + + if (null === $key) { + throw new Exception\InvalidArgumentException('No key specified for the decryption'); + } + + switch ($mode) { + case self::MODE_AUTO: + // check if data is encoded in Base64 + $output = base64_decode($data, true); + if ((false !== $output) && ($data === base64_encode($output))) { + $data = $output; + } + break; + case self::MODE_BASE64: + $data = base64_decode($data); + break; + case self::MODE_RAW: + default: + break; + } + + return $key->decrypt($data); + } + + /** + * Generate new private/public key pair + * @see RsaOptions::generateKeys() + * + * @param array $opensslConfig + * @return Rsa + * @throws Rsa\Exception\RuntimeException + */ + public function generateKeys(array $opensslConfig = array()) + { + $this->options->generateKeys($opensslConfig); + return $this; + } +} diff --git a/library/Zend/Crypt/PublicKey/Rsa/AbstractKey.php b/library/Zend/Crypt/PublicKey/Rsa/AbstractKey.php new file mode 100755 index 0000000000..5051c77164 --- /dev/null +++ b/library/Zend/Crypt/PublicKey/Rsa/AbstractKey.php @@ -0,0 +1,90 @@ +details['bits']; + } + + /** + * Retrieve openssl key resource + * + * @return resource + */ + public function getOpensslKeyResource() + { + return $this->opensslKeyResource; + } + + /** + * Encrypt using this key + * + * @abstract + * @param string $data + * @return string + */ + abstract public function encrypt($data); + + /** + * Decrypt using this key + * + * @abstract + * @param string $data + * @return string + */ + abstract public function decrypt($data); + + /** + * Get string representation of this key + * + * @abstract + * @return string + */ + abstract public function toString(); + + /** + * @return string + */ + public function __toString() + { + return $this->toString(); + } +} diff --git a/library/Zend/Crypt/PublicKey/Rsa/Exception/ExceptionInterface.php b/library/Zend/Crypt/PublicKey/Rsa/Exception/ExceptionInterface.php new file mode 100755 index 0000000000..ccaad1b39d --- /dev/null +++ b/library/Zend/Crypt/PublicKey/Rsa/Exception/ExceptionInterface.php @@ -0,0 +1,16 @@ +pemString = $pemString; + $this->opensslKeyResource = $result; + $this->details = openssl_pkey_get_details($this->opensslKeyResource); + } + + /** + * Get the public key + * + * @return PublicKey + */ + public function getPublicKey() + { + if ($this->publicKey === null) { + $this->publicKey = new PublicKey($this->details['key']); + } + + return $this->publicKey; + } + + /** + * Encrypt using this key + * + * @param string $data + * @return string + * @throws Exception\RuntimeException + * @throws Exception\InvalidArgumentException + */ + public function encrypt($data) + { + if (empty($data)) { + throw new Exception\InvalidArgumentException('The data to encrypt cannot be empty'); + } + + $encrypted = ''; + $result = openssl_private_encrypt($data, $encrypted, $this->getOpensslKeyResource()); + if (false === $result) { + throw new Exception\RuntimeException( + 'Can not encrypt; openssl ' . openssl_error_string() + ); + } + + return $encrypted; + } + + /** + * Decrypt using this key + * + * @param string $data + * @return string + * @throws Exception\RuntimeException + * @throws Exception\InvalidArgumentException + */ + public function decrypt($data) + { + if (!is_string($data)) { + throw new Exception\InvalidArgumentException('The data to decrypt must be a string'); + } + if ('' === $data) { + throw new Exception\InvalidArgumentException('The data to decrypt cannot be empty'); + } + + $decrypted = ''; + $result = openssl_private_decrypt($data, $decrypted, $this->getOpensslKeyResource()); + if (false === $result) { + throw new Exception\RuntimeException( + 'Can not decrypt; openssl ' . openssl_error_string() + ); + } + + return $decrypted; + } + + /** + * @return string + */ + public function toString() + { + return $this->pemString; + } +} diff --git a/library/Zend/Crypt/PublicKey/Rsa/PublicKey.php b/library/Zend/Crypt/PublicKey/Rsa/PublicKey.php new file mode 100755 index 0000000000..d73078d28a --- /dev/null +++ b/library/Zend/Crypt/PublicKey/Rsa/PublicKey.php @@ -0,0 +1,146 @@ +certificateString = $pemStringOrCertificate; + } else { + $this->pemString = $pemStringOrCertificate; + } + + $this->opensslKeyResource = $result; + $this->details = openssl_pkey_get_details($this->opensslKeyResource); + } + + /** + * Encrypt using this key + * + * @param string $data + * @throws Exception\InvalidArgumentException + * @throws Exception\RuntimeException + * @return string + */ + public function encrypt($data) + { + if (empty($data)) { + throw new Exception\InvalidArgumentException('The data to encrypt cannot be empty'); + } + + $encrypted = ''; + $result = openssl_public_encrypt($data, $encrypted, $this->getOpensslKeyResource()); + if (false === $result) { + throw new Exception\RuntimeException( + 'Can not encrypt; openssl ' . openssl_error_string() + ); + } + + return $encrypted; + } + + /** + * Decrypt using this key + * + * @param string $data + * @throws Exception\InvalidArgumentException + * @throws Exception\RuntimeException + * @return string + */ + public function decrypt($data) + { + if (!is_string($data)) { + throw new Exception\InvalidArgumentException('The data to decrypt must be a string'); + } + if ('' === $data) { + throw new Exception\InvalidArgumentException('The data to decrypt cannot be empty'); + } + + $decrypted = ''; + $result = openssl_public_decrypt($data, $decrypted, $this->getOpensslKeyResource()); + if (false === $result) { + throw new Exception\RuntimeException( + 'Can not decrypt; openssl ' . openssl_error_string() + ); + } + + return $decrypted; + } + + /** + * Get certificate string + * + * @return string + */ + public function getCertificate() + { + return $this->certificateString; + } + + /** + * To string + * + * @return string + * @throws Exception\RuntimeException + */ + public function toString() + { + if (!empty($this->certificateString)) { + return $this->certificateString; + } elseif (!empty($this->pemString)) { + return $this->pemString; + } + throw new Exception\RuntimeException('No public key string representation is available'); + } +} diff --git a/library/Zend/Crypt/PublicKey/RsaOptions.php b/library/Zend/Crypt/PublicKey/RsaOptions.php new file mode 100755 index 0000000000..54cd7c55b1 --- /dev/null +++ b/library/Zend/Crypt/PublicKey/RsaOptions.php @@ -0,0 +1,224 @@ +privateKey = $key; + $this->publicKey = $this->privateKey->getPublicKey(); + return $this; + } + + /** + * Get private key + * + * @return null|Rsa\PrivateKey + */ + public function getPrivateKey() + { + return $this->privateKey; + } + + /** + * Set public key + * + * @param Rsa\PublicKey $key + * @return RsaOptions + */ + public function setPublicKey(Rsa\PublicKey $key) + { + $this->publicKey = $key; + return $this; + } + + /** + * Get public key + * + * @return null|Rsa\PublicKey + */ + public function getPublicKey() + { + return $this->publicKey; + } + + /** + * Set pass phrase + * + * @param string $phrase + * @return RsaOptions + */ + public function setPassPhrase($phrase) + { + $this->passPhrase = (string) $phrase; + return $this; + } + + /** + * Get pass phrase + * + * @return string + */ + public function getPassPhrase() + { + return $this->passPhrase; + } + + /** + * Set hash algorithm + * + * @param string $hash + * @return RsaOptions + * @throws Rsa\Exception\RuntimeException + * @throws Rsa\Exception\InvalidArgumentException + */ + public function setHashAlgorithm($hash) + { + $hashUpper = strtoupper($hash); + if (!defined('OPENSSL_ALGO_' . $hashUpper)) { + throw new Exception\InvalidArgumentException( + "Hash algorithm '{$hash}' is not supported" + ); + } + + $this->hashAlgorithm = strtolower($hash); + $this->opensslSignatureAlgorithm = constant('OPENSSL_ALGO_' . $hashUpper); + return $this; + } + + /** + * Get hash algorithm + * + * @return string + */ + public function getHashAlgorithm() + { + return $this->hashAlgorithm; + } + + public function getOpensslSignatureAlgorithm() + { + if (!isset($this->opensslSignatureAlgorithm)) { + $this->opensslSignatureAlgorithm = constant('OPENSSL_ALGO_' . strtoupper($this->hashAlgorithm)); + } + return $this->opensslSignatureAlgorithm; + } + + /** + * Enable/disable the binary output + * + * @param bool $value + * @return RsaOptions + */ + public function setBinaryOutput($value) + { + $this->binaryOutput = (bool) $value; + return $this; + } + + /** + * Get the value of binary output + * + * @return bool + */ + public function getBinaryOutput() + { + return $this->binaryOutput; + } + + /** + * Generate new private/public key pair + * + * @param array $opensslConfig + * @return RsaOptions + * @throws Rsa\Exception\RuntimeException + */ + public function generateKeys(array $opensslConfig = array()) + { + $opensslConfig = array_replace( + array( + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + 'private_key_bits' => Rsa\PrivateKey::DEFAULT_KEY_SIZE, + 'digest_alg' => $this->getHashAlgorithm() + ), + $opensslConfig + ); + + // generate + $resource = openssl_pkey_new($opensslConfig); + if (false === $resource) { + throw new Exception\RuntimeException( + 'Can not generate keys; openssl ' . openssl_error_string() + ); + } + + // export key + $passPhrase = $this->getPassPhrase(); + $result = openssl_pkey_export($resource, $private, $passPhrase, $opensslConfig); + if (false === $result) { + throw new Exception\RuntimeException( + 'Can not export key; openssl ' . openssl_error_string() + ); + } + + $details = openssl_pkey_get_details($resource); + $this->privateKey = new Rsa\PrivateKey($private, $passPhrase); + $this->publicKey = new Rsa\PublicKey($details['key']); + + return $this; + } +} diff --git a/library/Zend/Crypt/README.md b/library/Zend/Crypt/README.md new file mode 100755 index 0000000000..1e2e5af919 --- /dev/null +++ b/library/Zend/Crypt/README.md @@ -0,0 +1,15 @@ +Crypt Component from ZF2 +======================== + +This is the Crypt component for ZF2. + +- File issues at https://github.com/zendframework/zf2/issues +- Create pull requests against https://github.com/zendframework/zf2 +- Documentation is at http://framework.zend.com/docs + +LICENSE +------- + +The files in this archive are released under the [Zend Framework +license](http://framework.zend.com/license), which is a 3-clause BSD license. + diff --git a/library/Zend/Crypt/Symmetric/Exception/ExceptionInterface.php b/library/Zend/Crypt/Symmetric/Exception/ExceptionInterface.php new file mode 100755 index 0000000000..7e62c51b20 --- /dev/null +++ b/library/Zend/Crypt/Symmetric/Exception/ExceptionInterface.php @@ -0,0 +1,16 @@ + 'rijndael-128', + 'blowfish' => 'blowfish', + 'des' => 'des', + '3des' => 'tripledes', + 'tripledes' => 'tripledes', + 'cast-128' => 'cast-128', + 'cast-256' => 'cast-256', + 'rijndael-128' => 'rijndael-128', + 'rijndael-192' => 'rijndael-192', + 'rijndael-256' => 'rijndael-256', + 'saferplus' => 'saferplus', + 'serpent' => 'serpent', + 'twofish' => 'twofish' + ); + + /** + * Supported encryption modes + * + * @var array + */ + protected $supportedModes = array( + 'cbc' => 'cbc', + 'cfb' => 'cfb', + 'ctr' => 'ctr', + 'ofb' => 'ofb', + 'nofb' => 'nofb', + 'ncfb' => 'ncfb' + ); + + /** + * Constructor + * + * @param array|Traversable $options + * @throws Exception\RuntimeException + * @throws Exception\InvalidArgumentException + */ + public function __construct($options = array()) + { + if (!extension_loaded('mcrypt')) { + throw new Exception\RuntimeException( + 'You cannot use ' . __CLASS__ . ' without the Mcrypt extension' + ); + } + if (!empty($options)) { + if ($options instanceof Traversable) { + $options = ArrayUtils::iteratorToArray($options); + } elseif (!is_array($options)) { + throw new Exception\InvalidArgumentException( + 'The options parameter must be an array, a Zend\Config\Config object or a Traversable' + ); + } + foreach ($options as $key => $value) { + switch (strtolower($key)) { + case 'algo': + case 'algorithm': + $this->setAlgorithm($value); + break; + case 'mode': + $this->setMode($value); + break; + case 'key': + $this->setKey($value); + break; + case 'iv': + case 'salt': + $this->setSalt($value); + break; + case 'padding': + $plugins = static::getPaddingPluginManager(); + $padding = $plugins->get($value); + $this->padding = $padding; + break; + } + } + } + $this->setDefaultOptions($options); + } + + /** + * Set default options + * + * @param array $options + * @return void + */ + protected function setDefaultOptions($options = array()) + { + if (!isset($options['padding'])) { + $plugins = static::getPaddingPluginManager(); + $padding = $plugins->get(self::DEFAULT_PADDING); + $this->padding = $padding; + } + } + + /** + * Returns the padding plugin manager. If it doesn't exist it's created. + * + * @return PaddingPluginManager + */ + public static function getPaddingPluginManager() + { + if (static::$paddingPlugins === null) { + self::setPaddingPluginManager(new PaddingPluginManager()); + } + + return static::$paddingPlugins; + } + + /** + * Set the padding plugin manager + * + * @param string|PaddingPluginManager $plugins + * @throws Exception\InvalidArgumentException + * @return void + */ + public static function setPaddingPluginManager($plugins) + { + if (is_string($plugins)) { + if (!class_exists($plugins)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Unable to locate padding plugin manager via class "%s"; class does not exist', + $plugins + )); + } + $plugins = new $plugins(); + } + if (!$plugins instanceof PaddingPluginManager) { + throw new Exception\InvalidArgumentException(sprintf( + 'Padding plugins must extend %s\PaddingPluginManager; received "%s"', + __NAMESPACE__, + (is_object($plugins) ? get_class($plugins) : gettype($plugins)) + )); + } + static::$paddingPlugins = $plugins; + } + + /** + * Get the maximum key size for the selected cipher and mode of operation + * + * @return int + */ + public function getKeySize() + { + return mcrypt_get_key_size($this->supportedAlgos[$this->algo], $this->supportedModes[$this->mode]); + } + + /** + * Set the encryption key + * If the key is longer than maximum supported, it will be truncated by getKey(). + * + * @param string $key + * @throws Exception\InvalidArgumentException + * @return Mcrypt + */ + public function setKey($key) + { + $keyLen = strlen($key); + + if (!$keyLen) { + throw new Exception\InvalidArgumentException('The key cannot be empty'); + } + $keySizes = mcrypt_module_get_supported_key_sizes($this->supportedAlgos[$this->algo]); + $maxKey = $this->getKeySize(); + + /* + * blowfish has $keySizes empty, meaning it can have arbitrary key length. + * the others are more picky. + */ + if (!empty($keySizes) && $keyLen < $maxKey) { + if (!in_array($keyLen, $keySizes)) { + throw new Exception\InvalidArgumentException( + "The size of the key must be one of " . implode(", ", $keySizes) . " bytes or longer" + ); + } + } + $this->key = $key; + + return $this; + } + + /** + * Get the encryption key + * + * @return string + */ + public function getKey() + { + if (empty($this->key)) { + return null; + } + return substr($this->key, 0, $this->getKeySize()); + } + + /** + * Set the encryption algorithm (cipher) + * + * @param string $algo + * @throws Exception\InvalidArgumentException + * @return Mcrypt + */ + public function setAlgorithm($algo) + { + if (!array_key_exists($algo, $this->supportedAlgos)) { + throw new Exception\InvalidArgumentException( + "The algorithm $algo is not supported by " . __CLASS__ + ); + } + $this->algo = $algo; + + return $this; + } + + /** + * Get the encryption algorithm + * + * @return string + */ + public function getAlgorithm() + { + return $this->algo; + } + + /** + * Set the padding object + * + * @param Padding\PaddingInterface $padding + * @return Mcrypt + */ + public function setPadding(Padding\PaddingInterface $padding) + { + $this->padding = $padding; + + return $this; + } + + /** + * Get the padding object + * + * @return Padding\PaddingInterface + */ + public function getPadding() + { + return $this->padding; + } + + /** + * Encrypt + * + * @param string $data + * @throws Exception\InvalidArgumentException + * @return string + */ + public function encrypt($data) + { + // Cannot encrypt empty string + if (!is_string($data) || $data === '') { + throw new Exception\InvalidArgumentException('The data to encrypt cannot be empty'); + } + if (null === $this->getKey()) { + throw new Exception\InvalidArgumentException('No key specified for the encryption'); + } + if (null === $this->getSalt()) { + throw new Exception\InvalidArgumentException('The salt (IV) cannot be empty'); + } + if (null === $this->getPadding()) { + throw new Exception\InvalidArgumentException('You have to specify a padding method'); + } + // padding + $data = $this->padding->pad($data, $this->getBlockSize()); + $iv = $this->getSalt(); + // encryption + $result = mcrypt_encrypt( + $this->supportedAlgos[$this->algo], + $this->getKey(), + $data, + $this->supportedModes[$this->mode], + $iv + ); + + return $iv . $result; + } + + /** + * Decrypt + * + * @param string $data + * @throws Exception\InvalidArgumentException + * @return string + */ + public function decrypt($data) + { + if (empty($data)) { + throw new Exception\InvalidArgumentException('The data to decrypt cannot be empty'); + } + if (null === $this->getKey()) { + throw new Exception\InvalidArgumentException('No key specified for the decryption'); + } + if (null === $this->getPadding()) { + throw new Exception\InvalidArgumentException('You have to specify a padding method'); + } + $iv = substr($data, 0, $this->getSaltSize()); + $ciphertext = substr($data, $this->getSaltSize()); + $result = mcrypt_decrypt( + $this->supportedAlgos[$this->algo], + $this->getKey(), + $ciphertext, + $this->supportedModes[$this->mode], + $iv + ); + // unpadding + return $this->padding->strip($result); + } + + /** + * Get the salt (IV) size + * + * @return int + */ + public function getSaltSize() + { + return mcrypt_get_iv_size($this->supportedAlgos[$this->algo], $this->supportedModes[$this->mode]); + } + + /** + * Get the supported algorithms + * + * @return array + */ + public function getSupportedAlgorithms() + { + return array_keys($this->supportedAlgos); + } + + /** + * Set the salt (IV) + * + * @param string $salt + * @throws Exception\InvalidArgumentException + * @return Mcrypt + */ + public function setSalt($salt) + { + if (empty($salt)) { + throw new Exception\InvalidArgumentException('The salt (IV) cannot be empty'); + } + if (strlen($salt) < $this->getSaltSize()) { + throw new Exception\InvalidArgumentException( + 'The size of the salt (IV) must be at least ' . $this->getSaltSize() . ' bytes' + ); + } + $this->iv = $salt; + + return $this; + } + + /** + * Get the salt (IV) according to the size requested by the algorithm + * + * @return string + */ + public function getSalt() + { + if (empty($this->iv)) { + return null; + } + if (strlen($this->iv) < $this->getSaltSize()) { + throw new Exception\RuntimeException( + 'The size of the salt (IV) must be at least ' . $this->getSaltSize() . ' bytes' + ); + } + + return substr($this->iv, 0, $this->getSaltSize()); + } + + /** + * Get the original salt value + * + * @return string + */ + public function getOriginalSalt() + { + return $this->iv; + } + + /** + * Set the cipher mode + * + * @param string $mode + * @throws Exception\InvalidArgumentException + * @return Mcrypt + */ + public function setMode($mode) + { + if (!empty($mode)) { + $mode = strtolower($mode); + if (!array_key_exists($mode, $this->supportedModes)) { + throw new Exception\InvalidArgumentException( + "The mode $mode is not supported by " . __CLASS__ + ); + } + $this->mode = $mode; + } + + return $this; + } + + /** + * Get the cipher mode + * + * @return string + */ + public function getMode() + { + return $this->mode; + } + + /** + * Get all supported encryption modes + * + * @return array + */ + public function getSupportedModes() + { + return array_keys($this->supportedModes); + } + + /** + * Get the block size + * + * @return int + */ + public function getBlockSize() + { + return mcrypt_get_block_size($this->supportedAlgos[$this->algo], $this->supportedModes[$this->mode]); + } +} diff --git a/library/Zend/Crypt/Symmetric/Padding/PaddingInterface.php b/library/Zend/Crypt/Symmetric/Padding/PaddingInterface.php new file mode 100755 index 0000000000..24050c311c --- /dev/null +++ b/library/Zend/Crypt/Symmetric/Padding/PaddingInterface.php @@ -0,0 +1,30 @@ + 'Zend\Crypt\Symmetric\Padding\Pkcs7' + ); + + /** + * Do not share by default + * + * @var bool + */ + protected $shareByDefault = false; + + /** + * Validate the plugin + * + * Checks that the padding adapter loaded is an instance of Padding\PaddingInterface. + * + * @param mixed $plugin + * @return void + * @throws Exception\InvalidArgumentException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof Padding\PaddingInterface) { + // we're okay + return; + } + + throw new Exception\InvalidArgumentException(sprintf( + 'Plugin of type %s is invalid; must implement %s\Padding\PaddingInterface', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/library/Zend/Crypt/Symmetric/SymmetricInterface.php b/library/Zend/Crypt/Symmetric/SymmetricInterface.php new file mode 100755 index 0000000000..acf160e966 --- /dev/null +++ b/library/Zend/Crypt/Symmetric/SymmetricInterface.php @@ -0,0 +1,61 @@ + 'Zend\Crypt\Symmetric\Mcrypt', + ); + + /** + * Do not share by default + * + * @var bool + */ + protected $shareByDefault = false; + + /** + * Validate the plugin + * + * Checks that the adapter loaded is an instance + * of Symmetric\SymmetricInterface. + * + * @param mixed $plugin + * @return void + * @throws Exception\InvalidArgumentException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof Symmetric\SymmetricInterface) { + // we're okay + return; + } + + throw new Exception\InvalidArgumentException(sprintf( + 'Plugin of type %s is invalid; must implement %s\Symmetric\SymmetricInterface', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/library/Zend/Crypt/Utils.php b/library/Zend/Crypt/Utils.php new file mode 100755 index 0000000000..ff013dcbca --- /dev/null +++ b/library/Zend/Crypt/Utils.php @@ -0,0 +1,45 @@ +=5.3.23", + "zendframework/zend-math": "self.version", + "zendframework/zend-stdlib": "self.version", + "zendframework/zend-servicemanager": "self.version" + }, + "suggest": { + "ext-mcrypt": "Required for most features of Zend\\Crypt" + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev", + "dev-develop": "2.4-dev" + } + } +} diff --git a/library/Zend/Db/Adapter/Adapter.php b/library/Zend/Db/Adapter/Adapter.php new file mode 100755 index 0000000000..b4c7edffff --- /dev/null +++ b/library/Zend/Db/Adapter/Adapter.php @@ -0,0 +1,387 @@ +createProfiler($parameters); + } + $driver = $this->createDriver($parameters); + } elseif (!$driver instanceof Driver\DriverInterface) { + throw new Exception\InvalidArgumentException( + 'The supplied or instantiated driver object does not implement Zend\Db\Adapter\Driver\DriverInterface' + ); + } + + $driver->checkEnvironment(); + $this->driver = $driver; + + if ($platform == null) { + $platform = $this->createPlatform($parameters); + } + + $this->platform = $platform; + $this->queryResultSetPrototype = ($queryResultPrototype) ?: new ResultSet\ResultSet(); + + if ($profiler) { + $this->setProfiler($profiler); + } + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return Adapter + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + if ($this->driver instanceof Profiler\ProfilerAwareInterface) { + $this->driver->setProfiler($profiler); + } + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * getDriver() + * + * @throws Exception\RuntimeException + * @return Driver\DriverInterface + */ + public function getDriver() + { + if ($this->driver == null) { + throw new Exception\RuntimeException('Driver has not been set or configured for this adapter.'); + } + return $this->driver; + } + + /** + * @return Platform\PlatformInterface + */ + public function getPlatform() + { + return $this->platform; + } + + /** + * @return ResultSet\ResultSetInterface + */ + public function getQueryResultSetPrototype() + { + return $this->queryResultSetPrototype; + } + + public function getCurrentSchema() + { + return $this->driver->getConnection()->getCurrentSchema(); + } + + /** + * query() is a convenience function + * + * @param string $sql + * @param string|array|ParameterContainer $parametersOrQueryMode + * @throws Exception\InvalidArgumentException + * @return Driver\StatementInterface|ResultSet\ResultSet + */ + public function query($sql, $parametersOrQueryMode = self::QUERY_MODE_PREPARE, ResultSet\ResultSetInterface $resultPrototype = null) + { + if (is_string($parametersOrQueryMode) && in_array($parametersOrQueryMode, array(self::QUERY_MODE_PREPARE, self::QUERY_MODE_EXECUTE))) { + $mode = $parametersOrQueryMode; + $parameters = null; + } elseif (is_array($parametersOrQueryMode) || $parametersOrQueryMode instanceof ParameterContainer) { + $mode = self::QUERY_MODE_PREPARE; + $parameters = $parametersOrQueryMode; + } else { + throw new Exception\InvalidArgumentException('Parameter 2 to this method must be a flag, an array, or ParameterContainer'); + } + + if ($mode == self::QUERY_MODE_PREPARE) { + $this->lastPreparedStatement = null; + $this->lastPreparedStatement = $this->driver->createStatement($sql); + $this->lastPreparedStatement->prepare(); + if (is_array($parameters) || $parameters instanceof ParameterContainer) { + $this->lastPreparedStatement->setParameterContainer((is_array($parameters)) ? new ParameterContainer($parameters) : $parameters); + $result = $this->lastPreparedStatement->execute(); + } else { + return $this->lastPreparedStatement; + } + } else { + $result = $this->driver->getConnection()->execute($sql); + } + + if ($result instanceof Driver\ResultInterface && $result->isQueryResult()) { + $resultSet = clone ($resultPrototype ?: $this->queryResultSetPrototype); + $resultSet->initialize($result); + return $resultSet; + } + + return $result; + } + + /** + * Create statement + * + * @param string $initialSql + * @param ParameterContainer $initialParameters + * @return Driver\StatementInterface + */ + public function createStatement($initialSql = null, $initialParameters = null) + { + $statement = $this->driver->createStatement($initialSql); + if ($initialParameters == null || !$initialParameters instanceof ParameterContainer && is_array($initialParameters)) { + $initialParameters = new ParameterContainer((is_array($initialParameters) ? $initialParameters : array())); + } + $statement->setParameterContainer($initialParameters); + return $statement; + } + + public function getHelpers(/* $functions */) + { + $functions = array(); + $platform = $this->platform; + foreach (func_get_args() as $arg) { + switch ($arg) { + case self::FUNCTION_QUOTE_IDENTIFIER: + $functions[] = function ($value) use ($platform) { return $platform->quoteIdentifier($value); }; + break; + case self::FUNCTION_QUOTE_VALUE: + $functions[] = function ($value) use ($platform) { return $platform->quoteValue($value); }; + break; + + } + } + } + + /** + * @param $name + * @throws Exception\InvalidArgumentException + * @return Driver\DriverInterface|Platform\PlatformInterface + */ + public function __get($name) + { + switch (strtolower($name)) { + case 'driver': + return $this->driver; + case 'platform': + return $this->platform; + default: + throw new Exception\InvalidArgumentException('Invalid magic property on adapter'); + } + } + + /** + * @param array $parameters + * @return Driver\DriverInterface + * @throws \InvalidArgumentException + * @throws Exception\InvalidArgumentException + */ + protected function createDriver($parameters) + { + if (!isset($parameters['driver'])) { + throw new Exception\InvalidArgumentException(__FUNCTION__ . ' expects a "driver" key to be present inside the parameters'); + } + + if ($parameters['driver'] instanceof Driver\DriverInterface) { + return $parameters['driver']; + } + + if (!is_string($parameters['driver'])) { + throw new Exception\InvalidArgumentException(__FUNCTION__ . ' expects a "driver" to be a string or instance of DriverInterface'); + } + + $options = array(); + if (isset($parameters['options'])) { + $options = (array) $parameters['options']; + unset($parameters['options']); + } + + $driverName = strtolower($parameters['driver']); + switch ($driverName) { + case 'mysqli': + $driver = new Driver\Mysqli\Mysqli($parameters, null, null, $options); + break; + case 'sqlsrv': + $driver = new Driver\Sqlsrv\Sqlsrv($parameters); + break; + case 'oci8': + $driver = new Driver\Oci8\Oci8($parameters); + break; + case 'pgsql': + $driver = new Driver\Pgsql\Pgsql($parameters); + break; + case 'ibmdb2': + $driver = new Driver\IbmDb2\IbmDb2($parameters); + break; + case 'pdo': + default: + if ($driverName == 'pdo' || strpos($driverName, 'pdo') === 0) { + $driver = new Driver\Pdo\Pdo($parameters); + } + } + + if (!isset($driver) || !$driver instanceof Driver\DriverInterface) { + throw new Exception\InvalidArgumentException('DriverInterface expected', null, null); + } + + return $driver; + } + + /** + * @param Driver\DriverInterface $driver + * @return Platform\PlatformInterface + */ + protected function createPlatform($parameters) + { + if (isset($parameters['platform'])) { + $platformName = $parameters['platform']; + } elseif ($this->driver instanceof Driver\DriverInterface) { + $platformName = $this->driver->getDatabasePlatformName(Driver\DriverInterface::NAME_FORMAT_CAMELCASE); + } else { + throw new Exception\InvalidArgumentException('A platform could not be determined from the provided configuration'); + } + + // currently only supported by the IbmDb2 & Oracle concrete implementations + $options = (isset($parameters['platform_options'])) ? $parameters['platform_options'] : array(); + + switch ($platformName) { + case 'Mysql': + // mysqli or pdo_mysql driver + $driver = ($this->driver instanceof Driver\Mysqli\Mysqli || $this->driver instanceof Driver\Pdo\Pdo) ? $this->driver : null; + return new Platform\Mysql($driver); + case 'SqlServer': + // PDO is only supported driver for quoting values in this platform + return new Platform\SqlServer(($this->driver instanceof Driver\Pdo\Pdo) ? $this->driver : null); + case 'Oracle': + // oracle does not accept a driver as an option, no driver specific quoting available + return new Platform\Oracle($options); + case 'Sqlite': + // PDO is only supported driver for quoting values in this platform + return new Platform\Sqlite(($this->driver instanceof Driver\Pdo\Pdo) ? $this->driver : null); + case 'Postgresql': + // pgsql or pdo postgres driver + $driver = ($this->driver instanceof Driver\Pgsql\Pgsql || $this->driver instanceof Driver\Pdo\Pdo) ? $this->driver : null; + return new Platform\Postgresql($driver); + case 'IbmDb2': + // ibm_db2 driver escaping does not need an action connection + return new Platform\IbmDb2($options); + default: + return new Platform\Sql92(); + } + } + + protected function createProfiler($parameters) + { + if ($parameters['profiler'] instanceof Profiler\ProfilerInterface) { + $profiler = $parameters['profiler']; + } elseif (is_bool($parameters['profiler'])) { + $profiler = ($parameters['profiler'] == true) ? new Profiler\Profiler : null; + } else { + throw new Exception\InvalidArgumentException( + '"profiler" parameter must be an instance of ProfilerInterface or a boolean' + ); + } + return $profiler; + } + + /** + * @param array $parameters + * @return Driver\DriverInterface + * @throws \InvalidArgumentException + * @throws Exception\InvalidArgumentException + * @deprecated + */ + protected function createDriverFromParameters(array $parameters) + { + return $this->createDriver($parameters); + } + + /** + * @param Driver\DriverInterface $driver + * @return Platform\PlatformInterface + * @deprecated + */ + protected function createPlatformFromDriver(Driver\DriverInterface $driver) + { + return $this->createPlatform($driver); + } +} diff --git a/library/Zend/Db/Adapter/AdapterAbstractServiceFactory.php b/library/Zend/Db/Adapter/AdapterAbstractServiceFactory.php new file mode 100755 index 0000000000..42800a2ce9 --- /dev/null +++ b/library/Zend/Db/Adapter/AdapterAbstractServiceFactory.php @@ -0,0 +1,99 @@ +getConfig($services); + if (empty($config)) { + return false; + } + + return ( + isset($config[$requestedName]) + && is_array($config[$requestedName]) + && !empty($config[$requestedName]) + ); + } + + /** + * Create a DB adapter + * + * @param ServiceLocatorInterface $services + * @param string $name + * @param string $requestedName + * @return Adapter + */ + public function createServiceWithName(ServiceLocatorInterface $services, $name, $requestedName) + { + $config = $this->getConfig($services); + return new Adapter($config[$requestedName]); + } + + /** + * Get db configuration, if any + * + * @param ServiceLocatorInterface $services + * @return array + */ + protected function getConfig(ServiceLocatorInterface $services) + { + if ($this->config !== null) { + return $this->config; + } + + if (!$services->has('Config')) { + $this->config = array(); + return $this->config; + } + + $config = $services->get('Config'); + if (!isset($config['db']) + || !is_array($config['db']) + ) { + $this->config = array(); + return $this->config; + } + + $config = $config['db']; + if (!isset($config['adapters']) + || !is_array($config['adapters']) + ) { + $this->config = array(); + return $this->config; + } + + $this->config = $config['adapters']; + return $this->config; + } +} diff --git a/library/Zend/Db/Adapter/AdapterAwareInterface.php b/library/Zend/Db/Adapter/AdapterAwareInterface.php new file mode 100755 index 0000000000..95443a9a0d --- /dev/null +++ b/library/Zend/Db/Adapter/AdapterAwareInterface.php @@ -0,0 +1,21 @@ +adapter = $adapter; + + return $this; + } +} diff --git a/library/Zend/Db/Adapter/AdapterInterface.php b/library/Zend/Db/Adapter/AdapterInterface.php new file mode 100755 index 0000000000..57a6a73763 --- /dev/null +++ b/library/Zend/Db/Adapter/AdapterInterface.php @@ -0,0 +1,28 @@ +get('Config'); + return new Adapter($config['db']); + } +} diff --git a/library/Zend/Db/Adapter/Driver/ConnectionInterface.php b/library/Zend/Db/Adapter/Driver/ConnectionInterface.php new file mode 100755 index 0000000000..2e27fd6889 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/ConnectionInterface.php @@ -0,0 +1,85 @@ +driver = $driver; + } + + /** + * Get name + * + * @return string + */ + abstract public function getName(); +} diff --git a/library/Zend/Db/Adapter/Driver/Feature/DriverFeatureInterface.php b/library/Zend/Db/Adapter/Driver/Feature/DriverFeatureInterface.php new file mode 100755 index 0000000000..10c96f7731 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Feature/DriverFeatureInterface.php @@ -0,0 +1,37 @@ +setConnectionParameters($connectionParameters); + } elseif (is_resource($connectionParameters)) { + $this->setResource($connectionParameters); + } elseif (null !== $connectionParameters) { + throw new Exception\InvalidArgumentException( + '$connection must be an array of parameters, a db2 connection resource or null' + ); + } + } + + /** + * Set driver + * + * @param IbmDb2 $driver + * @return Connection + */ + public function setDriver(IbmDb2 $driver) + { + $this->driver = $driver; + return $this; + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return Connection + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * @param array $connectionParameters + * @return Connection + */ + public function setConnectionParameters(array $connectionParameters) + { + $this->connectionParameters = $connectionParameters; + return $this; + } + + /** + * @return array + */ + public function getConnectionParameters() + { + return $this->connectionParameters; + } + + /** + * @param resource $resource DB2 resource + * @return Connection + */ + public function setResource($resource) + { + if (!is_resource($resource) || get_resource_type($resource) !== 'DB2 Connection') { + throw new Exception\InvalidArgumentException('The resource provided must be of type "DB2 Connection"'); + } + $this->resource = $resource; + return $this; + } + + /** + * Get current schema + * + * @return string + */ + public function getCurrentSchema() + { + if (!$this->isConnected()) { + $this->connect(); + } + + $info = db2_server_info($this->resource); + return (isset($info->DB_NAME) ? $info->DB_NAME : ''); + } + + /** + * Get resource + * + * @return mixed + */ + public function getResource() + { + return $this->resource; + } + + /** + * Connect + * + * @return self + */ + public function connect() + { + if (is_resource($this->resource)) { + return $this; + } + + // localize + $p = $this->connectionParameters; + + // given a list of key names, test for existence in $p + $findParameterValue = function (array $names) use ($p) { + foreach ($names as $name) { + if (isset($p[$name])) { + return $p[$name]; + } + } + + return null; + }; + + $database = $findParameterValue(array('database', 'db')); + $username = $findParameterValue(array('username', 'uid', 'UID')); + $password = $findParameterValue(array('password', 'pwd', 'PWD')); + $isPersistent = $findParameterValue(array('persistent', 'PERSISTENT', 'Persistent')); + $options = (isset($p['driver_options']) ? $p['driver_options'] : array()); + $connect = ((bool) $isPersistent) ? 'db2_pconnect' : 'db2_connect'; + + $this->resource = $connect($database, $username, $password, $options); + + if ($this->resource === false) { + throw new Exception\RuntimeException(sprintf( + '%s: Unable to connect to database', + __METHOD__ + )); + } + + return $this; + } + + /** + * Is connected + * + * @return bool + */ + public function isConnected() + { + return ($this->resource !== null); + } + + /** + * Disconnect + * + * @return ConnectionInterface + */ + public function disconnect() + { + if ($this->resource) { + db2_close($this->resource); + $this->resource = null; + } + + return $this; + } + + /** + * Begin transaction + * + * @return ConnectionInterface + */ + public function beginTransaction() + { + if ($this->isI5() && !ini_get('ibm_db2.i5_allow_commit')) { + throw new Exception\RuntimeException( + 'DB2 transactions are not enabled, you need to set the ibm_db2.i5_allow_commit=1 in your php.ini' + ); + } + + if (!$this->isConnected()) { + $this->connect(); + } + + $this->prevAutocommit = db2_autocommit($this->resource); + db2_autocommit($this->resource, DB2_AUTOCOMMIT_OFF); + $this->inTransaction = true; + return $this; + } + + /** + * In transaction + * + * @return bool + */ + public function inTransaction() + { + return $this->inTransaction; + } + + /** + * Commit + * + * @return ConnectionInterface + */ + public function commit() + { + if (!$this->isConnected()) { + $this->connect(); + } + + if (!db2_commit($this->resource)) { + throw new Exception\RuntimeException("The commit has not been successful"); + } + + if ($this->prevAutocommit) { + db2_autocommit($this->resource, $this->prevAutocommit); + } + + $this->inTransaction = false; + return $this; + } + + /** + * Rollback + * + * @return ConnectionInterface + */ + public function rollback() + { + if (!$this->resource) { + throw new Exception\RuntimeException('Must be connected before you can rollback.'); + } + + if (!$this->inTransaction) { + throw new Exception\RuntimeException('Must call beginTransaction() before you can rollback.'); + } + + if (!db2_rollback($this->resource)) { + throw new Exception\RuntimeException('The rollback has not been successful'); + } + + if ($this->prevAutocommit) { + db2_autocommit($this->resource, $this->prevAutocommit); + } + + $this->inTransaction = false; + return $this; + } + + /** + * Execute + * + * @param string $sql + * @return Result + */ + public function execute($sql) + { + if (!$this->isConnected()) { + $this->connect(); + } + + if ($this->profiler) { + $this->profiler->profilerStart($sql); + } + + set_error_handler(function () {}, E_WARNING); // suppress warnings + $resultResource = db2_exec($this->resource, $sql); + restore_error_handler(); + + if ($this->profiler) { + $this->profiler->profilerFinish($sql); + } + + // if the returnValue is something other than a pg result resource, bypass wrapping it + if ($resultResource === false) { + throw new Exception\InvalidQueryException(db2_stmt_errormsg()); + } + + return $this->driver->createResult(($resultResource === true) ? $this->resource : $resultResource); + } + + /** + * Get last generated id + * + * @param null $name Ignored + * @return int + */ + public function getLastGeneratedValue($name = null) + { + return db2_last_insert_id($this->resource); + } + + /** + * Determine if the OS is OS400 (AS400, IBM i) + * + * @return bool + */ + protected function isI5() + { + if (isset($this->i5)) { + return $this->i5; + } + + $this->i5 = php_uname('s') == 'OS400' ? true : false; + return $this->i5; + } +} diff --git a/library/Zend/Db/Adapter/Driver/IbmDb2/IbmDb2.php b/library/Zend/Db/Adapter/Driver/IbmDb2/IbmDb2.php new file mode 100755 index 0000000000..d129b49b38 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/IbmDb2/IbmDb2.php @@ -0,0 +1,214 @@ +registerConnection($connection); + $this->registerStatementPrototype(($statementPrototype) ?: new Statement()); + $this->registerResultPrototype(($resultPrototype) ?: new Result()); + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return IbmDb2 + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + if ($this->connection instanceof Profiler\ProfilerAwareInterface) { + $this->connection->setProfiler($profiler); + } + if ($this->statementPrototype instanceof Profiler\ProfilerAwareInterface) { + $this->statementPrototype->setProfiler($profiler); + } + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * @param Connection $connection + * @return IbmDb2 + */ + public function registerConnection(Connection $connection) + { + $this->connection = $connection; + $this->connection->setDriver($this); + return $this; + } + + /** + * @param Statement $statementPrototype + * @return IbmDb2 + */ + public function registerStatementPrototype(Statement $statementPrototype) + { + $this->statementPrototype = $statementPrototype; + $this->statementPrototype->setDriver($this); + return $this; + } + + /** + * @param Result $resultPrototype + * @return IbmDb2 + */ + public function registerResultPrototype(Result $resultPrototype) + { + $this->resultPrototype = $resultPrototype; + return $this; + } + + /** + * Get database platform name + * + * @param string $nameFormat + * @return string + */ + public function getDatabasePlatformName($nameFormat = self::NAME_FORMAT_CAMELCASE) + { + if ($nameFormat == self::NAME_FORMAT_CAMELCASE) { + return 'IbmDb2'; + } else { + return 'IBM DB2'; + } + } + + /** + * Check environment + * + * @return bool + */ + public function checkEnvironment() + { + if (!extension_loaded('ibm_db2')) { + throw new Exception\RuntimeException('The ibm_db2 extension is required by this driver.'); + } + } + + /** + * Get connection + * + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * Create statement + * + * @param string|resource $sqlOrResource + * @return Statement + */ + public function createStatement($sqlOrResource = null) + { + $statement = clone $this->statementPrototype; + if (is_resource($sqlOrResource) && get_resource_type($sqlOrResource) == 'DB2 Statement') { + $statement->setResource($sqlOrResource); + } else { + if (is_string($sqlOrResource)) { + $statement->setSql($sqlOrResource); + } elseif ($sqlOrResource !== null) { + throw new Exception\InvalidArgumentException( + __FUNCTION__ . ' only accepts an SQL string or an ibm_db2 resource' + ); + } + if (!$this->connection->isConnected()) { + $this->connection->connect(); + } + $statement->initialize($this->connection->getResource()); + } + return $statement; + } + + /** + * Create result + * + * @param resource $resource + * @return Result + */ + public function createResult($resource) + { + $result = clone $this->resultPrototype; + $result->initialize($resource, $this->connection->getLastGeneratedValue()); + return $result; + } + + /** + * Get prepare type + * + * @return array + */ + public function getPrepareType() + { + return self::PARAMETERIZATION_POSITIONAL; + } + + /** + * Format parameter name + * + * @param string $name + * @param mixed $type + * @return string + */ + public function formatParameterName($name, $type = null) + { + return '?'; + } + + /** + * Get last generated value + * + * @return mixed + */ + public function getLastGeneratedValue() + { + return $this->connection->getLastGeneratedValue(); + } +} diff --git a/library/Zend/Db/Adapter/Driver/IbmDb2/Result.php b/library/Zend/Db/Adapter/Driver/IbmDb2/Result.php new file mode 100755 index 0000000000..add4e1e3f1 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/IbmDb2/Result.php @@ -0,0 +1,192 @@ +resource = $resource; + $this->generatedValue = $generatedValue; + return $this; + } + + /** + * (PHP 5 >= 5.0.0)
+ * Return the current element + * @link http://php.net/manual/en/iterator.current.php + * @return mixed Can return any type. + */ + public function current() + { + if ($this->currentComplete) { + return $this->currentData; + } + + $this->currentData = db2_fetch_assoc($this->resource); + return $this->currentData; + } + + /** + * @return mixed + */ + public function next() + { + $this->currentData = db2_fetch_assoc($this->resource); + $this->currentComplete = true; + $this->position++; + return $this->currentData; + } + + /** + * @return int|string + */ + public function key() + { + return $this->position; + } + + /** + * @return bool + */ + public function valid() + { + return ($this->currentData !== false); + } + + /** + * (PHP 5 >= 5.0.0)
+ * Rewind the Iterator to the first element + * @link http://php.net/manual/en/iterator.rewind.php + * @return void Any returned value is ignored. + */ + public function rewind() + { + if ($this->position > 0) { + throw new Exception\RuntimeException( + 'This result is a forward only result set, calling rewind() after moving forward is not supported' + ); + } + $this->currentData = db2_fetch_assoc($this->resource); + $this->currentComplete = true; + $this->position = 1; + } + + /** + * Force buffering + * + * @return void + */ + public function buffer() + { + return null; + } + + /** + * Check if is buffered + * + * @return bool|null + */ + public function isBuffered() + { + return false; + } + + /** + * Is query result? + * + * @return bool + */ + public function isQueryResult() + { + return (db2_num_fields($this->resource) > 0); + } + + /** + * Get affected rows + * + * @return int + */ + public function getAffectedRows() + { + return db2_num_rows($this->resource); + } + + /** + * Get generated value + * + * @return mixed|null + */ + public function getGeneratedValue() + { + return $this->generatedValue; + } + + /** + * Get the resource + * + * @return mixed + */ + public function getResource() + { + return $this->resource; + } + + /** + * Get field count + * + * @return int + */ + public function getFieldCount() + { + return db2_num_fields($this->resource); + } + + /** + * @return null|int + */ + public function count() + { + return null; + } +} diff --git a/library/Zend/Db/Adapter/Driver/IbmDb2/Statement.php b/library/Zend/Db/Adapter/Driver/IbmDb2/Statement.php new file mode 100755 index 0000000000..029a9ed276 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/IbmDb2/Statement.php @@ -0,0 +1,240 @@ +db2 = $resource; + return $this; + } + + /** + * @param IbmDb2 $driver + * @return Statement + */ + public function setDriver(IbmDb2 $driver) + { + $this->driver = $driver; + return $this; + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return Statement + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Set sql + * + * @param $sql + * @return mixed + */ + public function setSql($sql) + { + $this->sql = $sql; + return $this; + } + + /** + * Get sql + * + * @return mixed + */ + public function getSql() + { + return $this->sql; + } + + /** + * Set parameter container + * + * @param ParameterContainer $parameterContainer + * @return mixed + */ + public function setParameterContainer(ParameterContainer $parameterContainer) + { + $this->parameterContainer = $parameterContainer; + return $this; + } + + /** + * Get parameter container + * + * @return mixed + */ + public function getParameterContainer() + { + return $this->parameterContainer; + } + + /** + * @param $resource + * @throws \Zend\Db\Adapter\Exception\InvalidArgumentException + */ + public function setResource($resource) + { + if (get_resource_type($resource) !== 'DB2 Statement') { + throw new Exception\InvalidArgumentException('Resource must be of type DB2 Statement'); + } + $this->resource = $resource; + } + + /** + * Get resource + * + * @return resource + */ + public function getResource() + { + return $this->resource; + } + + /** + * Prepare sql + * + * @param string|null $sql + * @return Statement + */ + public function prepare($sql = null) + { + if ($this->isPrepared) { + throw new Exception\RuntimeException('This statement has been prepared already'); + } + + if ($sql == null) { + $sql = $this->sql; + } + + $this->resource = db2_prepare($this->db2, $sql); + + if ($this->resource === false) { + throw new Exception\RuntimeException(db2_stmt_errormsg(), db2_stmt_error()); + } + + $this->isPrepared = true; + return $this; + } + + /** + * Check if is prepared + * + * @return bool + */ + public function isPrepared() + { + return $this->isPrepared; + } + + /** + * Execute + * + * @param null $parameters + * @return Result + */ + public function execute($parameters = null) + { + if (!$this->isPrepared) { + $this->prepare(); + } + + /** START Standard ParameterContainer Merging Block */ + if (!$this->parameterContainer instanceof ParameterContainer) { + if ($parameters instanceof ParameterContainer) { + $this->parameterContainer = $parameters; + $parameters = null; + } else { + $this->parameterContainer = new ParameterContainer(); + } + } + + if (is_array($parameters)) { + $this->parameterContainer->setFromArray($parameters); + } + /** END Standard ParameterContainer Merging Block */ + + if ($this->profiler) { + $this->profiler->profilerStart($this); + } + + set_error_handler(function () {}, E_WARNING); // suppress warnings + $response = db2_execute($this->resource, $this->parameterContainer->getPositionalArray()); + restore_error_handler(); + + if ($this->profiler) { + $this->profiler->profilerFinish(); + } + + if ($response === false) { + throw new Exception\RuntimeException(db2_stmt_errormsg($this->resource)); + } + + $result = $this->driver->createResult($this->resource); + return $result; + } +} diff --git a/library/Zend/Db/Adapter/Driver/Mysqli/Connection.php b/library/Zend/Db/Adapter/Driver/Mysqli/Connection.php new file mode 100755 index 0000000000..d84db10fe2 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Mysqli/Connection.php @@ -0,0 +1,346 @@ +setConnectionParameters($connectionInfo); + } elseif ($connectionInfo instanceof \mysqli) { + $this->setResource($connectionInfo); + } elseif (null !== $connectionInfo) { + throw new Exception\InvalidArgumentException('$connection must be an array of parameters, a mysqli object or null'); + } + } + + /** + * @param Mysqli $driver + * @return Connection + */ + public function setDriver(Mysqli $driver) + { + $this->driver = $driver; + return $this; + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return Connection + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Set connection parameters + * + * @param array $connectionParameters + * @return Connection + */ + public function setConnectionParameters(array $connectionParameters) + { + $this->connectionParameters = $connectionParameters; + return $this; + } + + /** + * Get connection parameters + * + * @return array + */ + public function getConnectionParameters() + { + return $this->connectionParameters; + } + + /** + * Get current schema + * + * @return string + */ + public function getCurrentSchema() + { + if (!$this->isConnected()) { + $this->connect(); + } + + /** @var $result \mysqli_result */ + $result = $this->resource->query('SELECT DATABASE()'); + $r = $result->fetch_row(); + return $r[0]; + } + + /** + * Set resource + * + * @param \mysqli $resource + * @return Connection + */ + public function setResource(\mysqli $resource) + { + $this->resource = $resource; + return $this; + } + + /** + * Get resource + * + * @return \mysqli + */ + public function getResource() + { + $this->connect(); + return $this->resource; + } + + /** + * Connect + * + * @throws Exception\RuntimeException + * @return Connection + */ + public function connect() + { + if ($this->resource instanceof \mysqli) { + return $this; + } + + // localize + $p = $this->connectionParameters; + + // given a list of key names, test for existence in $p + $findParameterValue = function (array $names) use ($p) { + foreach ($names as $name) { + if (isset($p[$name])) { + return $p[$name]; + } + } + return; + }; + + $hostname = $findParameterValue(array('hostname', 'host')); + $username = $findParameterValue(array('username', 'user')); + $password = $findParameterValue(array('password', 'passwd', 'pw')); + $database = $findParameterValue(array('database', 'dbname', 'db', 'schema')); + $port = (isset($p['port'])) ? (int) $p['port'] : null; + $socket = (isset($p['socket'])) ? $p['socket'] : null; + + $this->resource = new \mysqli(); + $this->resource->init(); + + if (!empty($p['driver_options'])) { + foreach ($p['driver_options'] as $option => $value) { + if (is_string($option)) { + $option = strtoupper($option); + if (!defined($option)) { + continue; + } + $option = constant($option); + } + $this->resource->options($option, $value); + } + } + + $this->resource->real_connect($hostname, $username, $password, $database, $port, $socket); + + if ($this->resource->connect_error) { + throw new Exception\RuntimeException( + 'Connection error', + null, + new Exception\ErrorException($this->resource->connect_error, $this->resource->connect_errno) + ); + } + + if (!empty($p['charset'])) { + $this->resource->set_charset($p['charset']); + } + + return $this; + } + + /** + * Is connected + * + * @return bool + */ + public function isConnected() + { + return ($this->resource instanceof \mysqli); + } + + /** + * Disconnect + * + * @return void + */ + public function disconnect() + { + if ($this->resource instanceof \mysqli) { + $this->resource->close(); + } + $this->resource = null; + } + + /** + * Begin transaction + * + * @return void + */ + public function beginTransaction() + { + if (!$this->isConnected()) { + $this->connect(); + } + + $this->resource->autocommit(false); + $this->inTransaction = true; + } + + /** + * In transaction + * + * @return bool + */ + public function inTransaction() + { + return $this->inTransaction; + } + + /** + * Commit + * + * @return void + */ + public function commit() + { + if (!$this->resource) { + $this->connect(); + } + + $this->resource->commit(); + $this->inTransaction = false; + $this->resource->autocommit(true); + } + + /** + * Rollback + * + * @throws Exception\RuntimeException + * @return Connection + */ + public function rollback() + { + if (!$this->resource) { + throw new Exception\RuntimeException('Must be connected before you can rollback.'); + } + + if (!$this->inTransaction) { + throw new Exception\RuntimeException('Must call beginTransaction() before you can rollback.'); + } + + $this->resource->rollback(); + $this->resource->autocommit(true); + return $this; + } + + /** + * Execute + * + * @param string $sql + * @throws Exception\InvalidQueryException + * @return Result + */ + public function execute($sql) + { + if (!$this->isConnected()) { + $this->connect(); + } + + if ($this->profiler) { + $this->profiler->profilerStart($sql); + } + + $resultResource = $this->resource->query($sql); + + if ($this->profiler) { + $this->profiler->profilerFinish($sql); + } + + // if the returnValue is something other than a mysqli_result, bypass wrapping it + if ($resultResource === false) { + throw new Exception\InvalidQueryException($this->resource->error); + } + + $resultPrototype = $this->driver->createResult(($resultResource === true) ? $this->resource : $resultResource); + return $resultPrototype; + } + + /** + * Get last generated id + * + * @param null $name Ignored + * @return int + */ + public function getLastGeneratedValue($name = null) + { + return $this->resource->insert_id; + } +} diff --git a/library/Zend/Db/Adapter/Driver/Mysqli/Mysqli.php b/library/Zend/Db/Adapter/Driver/Mysqli/Mysqli.php new file mode 100755 index 0000000000..443350ca10 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Mysqli/Mysqli.php @@ -0,0 +1,256 @@ + false + ); + + /** + * Constructor + * + * @param array|Connection|\mysqli $connection + * @param null|Statement $statementPrototype + * @param null|Result $resultPrototype + * @param array $options + */ + public function __construct($connection, Statement $statementPrototype = null, Result $resultPrototype = null, array $options = array()) + { + if (!$connection instanceof Connection) { + $connection = new Connection($connection); + } + + $options = array_intersect_key(array_merge($this->options, $options), $this->options); + + $this->registerConnection($connection); + $this->registerStatementPrototype(($statementPrototype) ?: new Statement($options['buffer_results'])); + $this->registerResultPrototype(($resultPrototype) ?: new Result()); + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return Mysqli + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + if ($this->connection instanceof Profiler\ProfilerAwareInterface) { + $this->connection->setProfiler($profiler); + } + if ($this->statementPrototype instanceof Profiler\ProfilerAwareInterface) { + $this->statementPrototype->setProfiler($profiler); + } + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Register connection + * + * @param Connection $connection + * @return Mysqli + */ + public function registerConnection(Connection $connection) + { + $this->connection = $connection; + $this->connection->setDriver($this); // needs access to driver to createStatement() + return $this; + } + + /** + * Register statement prototype + * + * @param Statement $statementPrototype + */ + public function registerStatementPrototype(Statement $statementPrototype) + { + $this->statementPrototype = $statementPrototype; + $this->statementPrototype->setDriver($this); // needs access to driver to createResult() + } + + /** + * Get statement prototype + * + * @return null|Statement + */ + public function getStatementPrototype() + { + return $this->statementPrototype; + } + + /** + * Register result prototype + * + * @param Result $resultPrototype + */ + public function registerResultPrototype(Result $resultPrototype) + { + $this->resultPrototype = $resultPrototype; + } + + /** + * @return null|Result + */ + public function getResultPrototype() + { + return $this->resultPrototype; + } + + /** + * Get database platform name + * + * @param string $nameFormat + * @return string + */ + public function getDatabasePlatformName($nameFormat = self::NAME_FORMAT_CAMELCASE) + { + if ($nameFormat == self::NAME_FORMAT_CAMELCASE) { + return 'Mysql'; + } + + return 'MySQL'; + } + + /** + * Check environment + * + * @throws Exception\RuntimeException + * @return void + */ + public function checkEnvironment() + { + if (!extension_loaded('mysqli')) { + throw new Exception\RuntimeException('The Mysqli extension is required for this adapter but the extension is not loaded'); + } + } + + /** + * Get connection + * + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * Create statement + * + * @param string $sqlOrResource + * @return Statement + */ + public function createStatement($sqlOrResource = null) + { + /** + * @todo Resource tracking + if (is_resource($sqlOrResource) && !in_array($sqlOrResource, $this->resources, true)) { + $this->resources[] = $sqlOrResource; + } + */ + + $statement = clone $this->statementPrototype; + if ($sqlOrResource instanceof mysqli_stmt) { + $statement->setResource($sqlOrResource); + } else { + if (is_string($sqlOrResource)) { + $statement->setSql($sqlOrResource); + } + if (!$this->connection->isConnected()) { + $this->connection->connect(); + } + $statement->initialize($this->connection->getResource()); + } + return $statement; + } + + /** + * Create result + * + * @param resource $resource + * @param null|bool $isBuffered + * @return Result + */ + public function createResult($resource, $isBuffered = null) + { + $result = clone $this->resultPrototype; + $result->initialize($resource, $this->connection->getLastGeneratedValue(), $isBuffered); + return $result; + } + + /** + * Get prepare type + * + * @return array + */ + public function getPrepareType() + { + return self::PARAMETERIZATION_POSITIONAL; + } + + /** + * Format parameter name + * + * @param string $name + * @param mixed $type + * @return string + */ + public function formatParameterName($name, $type = null) + { + return '?'; + } + + /** + * Get last generated value + * + * @return mixed + */ + public function getLastGeneratedValue() + { + return $this->getConnection()->getLastGeneratedValue(); + } +} diff --git a/library/Zend/Db/Adapter/Driver/Mysqli/Result.php b/library/Zend/Db/Adapter/Driver/Mysqli/Result.php new file mode 100755 index 0000000000..11622b15ac --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Mysqli/Result.php @@ -0,0 +1,341 @@ + null, 'values' => array()); + + /** + * @var mixed + */ + protected $generatedValue = null; + + /** + * Initialize + * + * @param mixed $resource + * @param mixed $generatedValue + * @param bool|null $isBuffered + * @throws Exception\InvalidArgumentException + * @return Result + */ + public function initialize($resource, $generatedValue, $isBuffered = null) + { + if (!$resource instanceof \mysqli && !$resource instanceof \mysqli_result && !$resource instanceof \mysqli_stmt) { + throw new Exception\InvalidArgumentException('Invalid resource provided.'); + } + + if ($isBuffered !== null) { + $this->isBuffered = $isBuffered; + } else { + if ($resource instanceof \mysqli || $resource instanceof \mysqli_result + || $resource instanceof \mysqli_stmt && $resource->num_rows != 0) { + $this->isBuffered = true; + } + } + + $this->resource = $resource; + $this->generatedValue = $generatedValue; + return $this; + } + + /** + * Force buffering + * + * @throws Exception\RuntimeException + */ + public function buffer() + { + if ($this->resource instanceof \mysqli_stmt && $this->isBuffered !== true) { + if ($this->position > 0) { + throw new Exception\RuntimeException('Cannot buffer a result set that has started iteration.'); + } + $this->resource->store_result(); + $this->isBuffered = true; + } + } + + /** + * Check if is buffered + * + * @return bool|null + */ + public function isBuffered() + { + return $this->isBuffered; + } + + /** + * Return the resource + * + * @return mixed + */ + public function getResource() + { + return $this->resource; + } + + /** + * Is query result? + * + * @return bool + */ + public function isQueryResult() + { + return ($this->resource->field_count > 0); + } + + /** + * Get affected rows + * + * @return int + */ + public function getAffectedRows() + { + if ($this->resource instanceof \mysqli || $this->resource instanceof \mysqli_stmt) { + return $this->resource->affected_rows; + } + + return $this->resource->num_rows; + } + + /** + * Current + * + * @return mixed + */ + public function current() + { + if ($this->currentComplete) { + return $this->currentData; + } + + if ($this->resource instanceof \mysqli_stmt) { + $this->loadDataFromMysqliStatement(); + return $this->currentData; + } else { + $this->loadFromMysqliResult(); + return $this->currentData; + } + } + + /** + * Mysqli's binding and returning of statement values + * + * Mysqli requires you to bind variables to the extension in order to + * get data out. These values have to be references: + * @see http://php.net/manual/en/mysqli-stmt.bind-result.php + * + * @throws Exception\RuntimeException + * @return bool + */ + protected function loadDataFromMysqliStatement() + { + $data = null; + // build the default reference based bind structure, if it does not already exist + if ($this->statementBindValues['keys'] === null) { + $this->statementBindValues['keys'] = array(); + $resultResource = $this->resource->result_metadata(); + foreach ($resultResource->fetch_fields() as $col) { + $this->statementBindValues['keys'][] = $col->name; + } + $this->statementBindValues['values'] = array_fill(0, count($this->statementBindValues['keys']), null); + $refs = array(); + foreach ($this->statementBindValues['values'] as $i => &$f) { + $refs[$i] = &$f; + } + call_user_func_array(array($this->resource, 'bind_result'), $this->statementBindValues['values']); + } + + if (($r = $this->resource->fetch()) === null) { + if (!$this->isBuffered) { + $this->resource->close(); + } + return false; + } elseif ($r === false) { + throw new Exception\RuntimeException($this->resource->error); + } + + // dereference + for ($i = 0, $count = count($this->statementBindValues['keys']); $i < $count; $i++) { + $this->currentData[$this->statementBindValues['keys'][$i]] = $this->statementBindValues['values'][$i]; + } + $this->currentComplete = true; + $this->nextComplete = true; + $this->position++; + return true; + } + + /** + * Load from mysqli result + * + * @return bool + */ + protected function loadFromMysqliResult() + { + $this->currentData = null; + + if (($data = $this->resource->fetch_assoc()) === null) { + return false; + } + + $this->position++; + $this->currentData = $data; + $this->currentComplete = true; + $this->nextComplete = true; + $this->position++; + return true; + } + + /** + * Next + * + * @return void + */ + public function next() + { + $this->currentComplete = false; + + if ($this->nextComplete == false) { + $this->position++; + } + + $this->nextComplete = false; + } + + /** + * Key + * + * @return mixed + */ + public function key() + { + return $this->position; + } + + /** + * Rewind + * + * @throws Exception\RuntimeException + * @return void + */ + public function rewind() + { + if ($this->position !== 0) { + if ($this->isBuffered === false) { + throw new Exception\RuntimeException('Unbuffered results cannot be rewound for multiple iterations'); + } + } + $this->resource->data_seek(0); // works for both mysqli_result & mysqli_stmt + $this->currentComplete = false; + $this->position = 0; + } + + /** + * Valid + * + * @return bool + */ + public function valid() + { + if ($this->currentComplete) { + return true; + } + + if ($this->resource instanceof \mysqli_stmt) { + return $this->loadDataFromMysqliStatement(); + } + + return $this->loadFromMysqliResult(); + } + + /** + * Count + * + * @throws Exception\RuntimeException + * @return int + */ + public function count() + { + if ($this->isBuffered === false) { + throw new Exception\RuntimeException('Row count is not available in unbuffered result sets.'); + } + return $this->resource->num_rows; + } + + /** + * Get field count + * + * @return int + */ + public function getFieldCount() + { + return $this->resource->field_count; + } + + /** + * Get generated value + * + * @return mixed|null + */ + public function getGeneratedValue() + { + return $this->generatedValue; + } +} diff --git a/library/Zend/Db/Adapter/Driver/Mysqli/Statement.php b/library/Zend/Db/Adapter/Driver/Mysqli/Statement.php new file mode 100755 index 0000000000..d462cd177e --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Mysqli/Statement.php @@ -0,0 +1,315 @@ +bufferResults = (bool) $bufferResults; + } + + /** + * Set driver + * + * @param Mysqli $driver + * @return Statement + */ + public function setDriver(Mysqli $driver) + { + $this->driver = $driver; + return $this; + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return Statement + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Initialize + * + * @param \mysqli $mysqli + * @return Statement + */ + public function initialize(\mysqli $mysqli) + { + $this->mysqli = $mysqli; + return $this; + } + + /** + * Set sql + * + * @param string $sql + * @return Statement + */ + public function setSql($sql) + { + $this->sql = $sql; + return $this; + } + + /** + * Set Parameter container + * + * @param ParameterContainer $parameterContainer + * @return Statement + */ + public function setParameterContainer(ParameterContainer $parameterContainer) + { + $this->parameterContainer = $parameterContainer; + return $this; + } + + /** + * Get resource + * + * @return mixed + */ + public function getResource() + { + return $this->resource; + } + + /** + * Set resource + * + * @param \mysqli_stmt $mysqliStatement + * @return Statement + */ + public function setResource(\mysqli_stmt $mysqliStatement) + { + $this->resource = $mysqliStatement; + $this->isPrepared = true; + return $this; + } + + /** + * Get sql + * + * @return string + */ + public function getSql() + { + return $this->sql; + } + + /** + * Get parameter count + * + * @return ParameterContainer + */ + public function getParameterContainer() + { + return $this->parameterContainer; + } + + /** + * Is prepared + * + * @return bool + */ + public function isPrepared() + { + return $this->isPrepared; + } + + /** + * Prepare + * + * @param string $sql + * @throws Exception\InvalidQueryException + * @throws Exception\RuntimeException + * @return Statement + */ + public function prepare($sql = null) + { + if ($this->isPrepared) { + throw new Exception\RuntimeException('This statement has already been prepared'); + } + + $sql = ($sql) ?: $this->sql; + + $this->resource = $this->mysqli->prepare($sql); + if (!$this->resource instanceof \mysqli_stmt) { + throw new Exception\InvalidQueryException( + 'Statement couldn\'t be produced with sql: ' . $sql, + null, + new Exception\ErrorException($this->mysqli->error, $this->mysqli->errno) + ); + } + + $this->isPrepared = true; + return $this; + } + + /** + * Execute + * + * @param ParameterContainer|array $parameters + * @throws Exception\RuntimeException + * @return mixed + */ + public function execute($parameters = null) + { + if (!$this->isPrepared) { + $this->prepare(); + } + + /** START Standard ParameterContainer Merging Block */ + if (!$this->parameterContainer instanceof ParameterContainer) { + if ($parameters instanceof ParameterContainer) { + $this->parameterContainer = $parameters; + $parameters = null; + } else { + $this->parameterContainer = new ParameterContainer(); + } + } + + if (is_array($parameters)) { + $this->parameterContainer->setFromArray($parameters); + } + + if ($this->parameterContainer->count() > 0) { + $this->bindParametersFromContainer(); + } + /** END Standard ParameterContainer Merging Block */ + + if ($this->profiler) { + $this->profiler->profilerStart($this); + } + + $return = $this->resource->execute(); + + if ($this->profiler) { + $this->profiler->profilerFinish(); + } + + if ($return === false) { + throw new Exception\RuntimeException($this->resource->error); + } + + if ($this->bufferResults === true) { + $this->resource->store_result(); + $this->isPrepared = false; + $buffered = true; + } else { + $buffered = false; + } + + $result = $this->driver->createResult($this->resource, $buffered); + return $result; + } + + /** + * Bind parameters from container + * + * @return void + */ + protected function bindParametersFromContainer() + { + $parameters = $this->parameterContainer->getNamedArray(); + $type = ''; + $args = array(); + + foreach ($parameters as $name => &$value) { + if ($this->parameterContainer->offsetHasErrata($name)) { + switch ($this->parameterContainer->offsetGetErrata($name)) { + case ParameterContainer::TYPE_DOUBLE: + $type .= 'd'; + break; + case ParameterContainer::TYPE_NULL: + $value = null; // as per @see http://www.php.net/manual/en/mysqli-stmt.bind-param.php#96148 + case ParameterContainer::TYPE_INTEGER: + $type .= 'i'; + break; + case ParameterContainer::TYPE_STRING: + default: + $type .= 's'; + break; + } + } else { + $type .= 's'; + } + $args[] = &$value; + } + + if ($args) { + array_unshift($args, $type); + call_user_func_array(array($this->resource, 'bind_param'), $args); + } + } +} diff --git a/library/Zend/Db/Adapter/Driver/Oci8/Connection.php b/library/Zend/Db/Adapter/Driver/Oci8/Connection.php new file mode 100755 index 0000000000..73376521e2 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Oci8/Connection.php @@ -0,0 +1,346 @@ +setConnectionParameters($connectionInfo); + } elseif ($connectionInfo instanceof \oci8) { + $this->setResource($connectionInfo); + } elseif (null !== $connectionInfo) { + throw new Exception\InvalidArgumentException('$connection must be an array of parameters, an oci8 resource or null'); + } + } + + /** + * @param Oci8 $driver + * @return Connection + */ + public function setDriver(Oci8 $driver) + { + $this->driver = $driver; + return $this; + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return Connection + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Set connection parameters + * + * @param array $connectionParameters + * @return Connection + */ + public function setConnectionParameters(array $connectionParameters) + { + $this->connectionParameters = $connectionParameters; + return $this; + } + + /** + * Get connection parameters + * + * @return array + */ + public function getConnectionParameters() + { + return $this->connectionParameters; + } + + /** + * Get current schema + * + * @return string + */ + public function getCurrentSchema() + { + if (!$this->isConnected()) { + $this->connect(); + } + + $query = "SELECT sys_context('USERENV', 'CURRENT_SCHEMA') as \"current_schema\" FROM DUAL"; + $stmt = oci_parse($this->resource, $query); + oci_execute($stmt); + $dbNameArray = oci_fetch_array($stmt, OCI_ASSOC); + return $dbNameArray['current_schema']; + } + + /** + * Set resource + * + * @param resource $resource + * @return Connection + */ + public function setResource($resource) + { + if (!is_resource($resource) || get_resource_type($resource) !== 'oci8 connection') { + throw new Exception\InvalidArgumentException('A resource of type "oci8 connection" was expected'); + } + $this->resource = $resource; + return $this; + } + + /** + * Get resource + * + * @return \oci8 + */ + public function getResource() + { + $this->connect(); + return $this->resource; + } + + /** + * Connect + * + * @return Connection + */ + public function connect() + { + if (is_resource($this->resource)) { + return $this; + } + + // localize + $p = $this->connectionParameters; + + // given a list of key names, test for existence in $p + $findParameterValue = function (array $names) use ($p) { + foreach ($names as $name) { + if (isset($p[$name])) { + return $p[$name]; + } + } + return null; + }; + + // http://www.php.net/manual/en/function.oci-connect.php + $username = $findParameterValue(array('username')); + $password = $findParameterValue(array('password')); + $connectionString = $findParameterValue(array('connection_string', 'connectionstring', 'connection', 'hostname', 'instance')); + $characterSet = $findParameterValue(array('character_set', 'charset', 'encoding')); + $sessionMode = $findParameterValue(array('session_mode')); + + // connection modifiers + $isUnique = $findParameterValue(array('unique')); + $isPersistent = $findParameterValue(array('persistent')); + + if ($isUnique == true) { + $this->resource = oci_new_connect($username, $password, $connectionString, $characterSet, $sessionMode); + } elseif ($isPersistent == true) { + $this->resource = oci_pconnect($username, $password, $connectionString, $characterSet, $sessionMode); + } else { + $this->resource = oci_connect($username, $password, $connectionString, $characterSet, $sessionMode); + } + + if (!$this->resource) { + $e = oci_error(); + throw new Exception\RuntimeException( + 'Connection error', + null, + new Exception\ErrorException($e['message'], $e['code']) + ); + } + + return $this; + } + + /** + * Is connected + * + * @return bool + */ + public function isConnected() + { + return (is_resource($this->resource)); + } + + /** + * Disconnect + */ + public function disconnect() + { + if (is_resource($this->resource)) { + oci_close($this->resource); + } + } + + /** + * Begin transaction + */ + public function beginTransaction() + { + if (!$this->isConnected()) { + $this->connect(); + } + + // A transaction begins when the first SQL statement that changes data is executed with oci_execute() using the OCI_NO_AUTO_COMMIT flag. + $this->inTransaction = true; + } + + /** + * In transaction + * + * @return bool + */ + public function inTransaction() + { + return $this->inTransaction; + } + + /** + * Commit + */ + public function commit() + { + if (!$this->resource) { + $this->connect(); + } + + if ($this->inTransaction) { + $valid = oci_commit($this->resource); + if ($valid === false) { + $e = oci_error($this->resource); + throw new Exception\InvalidQueryException($e['message'], $e['code']); + } + } + } + + /** + * Rollback + * + * @return Connection + */ + public function rollback() + { + if (!$this->resource) { + throw new Exception\RuntimeException('Must be connected before you can rollback.'); + } + + if (!$this->inTransaction) { + throw new Exception\RuntimeException('Must call commit() before you can rollback.'); + } + + $valid = oci_rollback($this->resource); + if ($valid === false) { + $e = oci_error($this->resource); + throw new Exception\InvalidQueryException($e['message'], $e['code']); + } + + return $this; + } + + /** + * Execute + * + * @param string $sql + * @return Result + */ + public function execute($sql) + { + if (!$this->isConnected()) { + $this->connect(); + } + + if ($this->profiler) { + $this->profiler->profilerStart($sql); + } + + $ociStmt = oci_parse($this->resource, $sql); + + if ($this->inTransaction) { + $valid = @oci_execute($ociStmt, OCI_NO_AUTO_COMMIT); + } else { + $valid = @oci_execute($ociStmt, OCI_COMMIT_ON_SUCCESS); + } + + if ($this->profiler) { + $this->profiler->profilerFinish($sql); + } + + if ($valid === false) { + $e = oci_error($ociStmt); + throw new Exception\InvalidQueryException($e['message'], $e['code']); + } + + $resultPrototype = $this->driver->createResult($ociStmt); + return $resultPrototype; + } + + /** + * Get last generated id + * + * @param null $name Ignored + * @return int + */ + public function getLastGeneratedValue($name = null) + { + // @todo Get Last Generated Value in Connection (this might not apply) + return null; + } +} diff --git a/library/Zend/Db/Adapter/Driver/Oci8/Oci8.php b/library/Zend/Db/Adapter/Driver/Oci8/Oci8.php new file mode 100755 index 0000000000..9685f8c41f --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Oci8/Oci8.php @@ -0,0 +1,223 @@ +registerConnection($connection); + $this->registerStatementPrototype(($statementPrototype) ?: new Statement()); + $this->registerResultPrototype(($resultPrototype) ?: new Result()); + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return Oci8 + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + if ($this->connection instanceof Profiler\ProfilerAwareInterface) { + $this->connection->setProfiler($profiler); + } + if ($this->statementPrototype instanceof Profiler\ProfilerAwareInterface) { + $this->statementPrototype->setProfiler($profiler); + } + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Register connection + * + * @param Connection $connection + * @return Oci8 + */ + public function registerConnection(Connection $connection) + { + $this->connection = $connection; + $this->connection->setDriver($this); // needs access to driver to createStatement() + return $this; + } + + /** + * Register statement prototype + * + * @param Statement $statementPrototype + * @return Oci8 + */ + public function registerStatementPrototype(Statement $statementPrototype) + { + $this->statementPrototype = $statementPrototype; + $this->statementPrototype->setDriver($this); // needs access to driver to createResult() + return $this; + } + + /** + * @return null|Statement + */ + public function getStatementPrototype() + { + return $this->statementPrototype; + } + + /** + * Register result prototype + * + * @param Result $resultPrototype + * @return Oci8 + */ + public function registerResultPrototype(Result $resultPrototype) + { + $this->resultPrototype = $resultPrototype; + return $this; + } + + /** + * @return null|Result + */ + public function getResultPrototype() + { + return $this->resultPrototype; + } + + /** + * Get database platform name + * + * @param string $nameFormat + * @return string + */ + public function getDatabasePlatformName($nameFormat = self::NAME_FORMAT_CAMELCASE) + { + return 'Oracle'; + } + + /** + * Check environment + */ + public function checkEnvironment() + { + if (!extension_loaded('oci8')) { + throw new Exception\RuntimeException('The Oci8 extension is required for this adapter but the extension is not loaded'); + } + } + + /** + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * @param string $sqlOrResource + * @return Statement + */ + public function createStatement($sqlOrResource = null) + { + $statement = clone $this->statementPrototype; + if (is_resource($sqlOrResource) && get_resource_type($sqlOrResource) == 'oci8 statement') { + $statement->setResource($sqlOrResource); + } else { + if (is_string($sqlOrResource)) { + $statement->setSql($sqlOrResource); + } elseif ($sqlOrResource !== null) { + throw new Exception\InvalidArgumentException( + 'Oci8 only accepts an SQL string or an oci8 resource in ' . __FUNCTION__ + ); + } + if (!$this->connection->isConnected()) { + $this->connection->connect(); + } + $statement->initialize($this->connection->getResource()); + } + return $statement; + } + + /** + * @param resource $resource + * @param null $isBuffered + * @return Result + */ + public function createResult($resource, $isBuffered = null) + { + $result = clone $this->resultPrototype; + $result->initialize($resource, $this->connection->getLastGeneratedValue(), $isBuffered); + return $result; + } + + /** + * @return array + */ + public function getPrepareType() + { + return self::PARAMETERIZATION_NAMED; + } + + /** + * @param string $name + * @param mixed $type + * @return string + */ + public function formatParameterName($name, $type = null) + { + return ':' . $name; + } + + /** + * @return mixed + */ + public function getLastGeneratedValue() + { + return $this->getConnection()->getLastGeneratedValue(); + } +} diff --git a/library/Zend/Db/Adapter/Driver/Oci8/Result.php b/library/Zend/Db/Adapter/Driver/Oci8/Result.php new file mode 100755 index 0000000000..f0ae96abd3 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Oci8/Result.php @@ -0,0 +1,224 @@ + null, 'values' => array()); + + /** + * @var mixed + */ + protected $generatedValue = null; + + /** + * Initialize + * @param resource $resource + * @return Result + */ + public function initialize($resource /*, $generatedValue, $isBuffered = null*/) + { + if (!is_resource($resource) && get_resource_type($resource) !== 'oci8 statement') { + throw new Exception\InvalidArgumentException('Invalid resource provided.'); + } + $this->resource = $resource; + return $this; + } + + /** + * Force buffering at driver level + * + * Oracle does not support this, to my knowledge (@ralphschindler) + * + * @throws Exception\RuntimeException + */ + public function buffer() + { + return null; + } + + /** + * Is the result buffered? + * + * @return bool + */ + public function isBuffered() + { + return false; + } + + /** + * Return the resource + * @return mixed + */ + public function getResource() + { + return $this->resource; + } + + /** + * Is query result? + * + * @return bool + */ + public function isQueryResult() + { + return (oci_num_fields($this->resource) > 0); + } + + /** + * Get affected rows + * @return int + */ + public function getAffectedRows() + { + return oci_num_rows($this->resource); + } + + /** + * Current + * @return mixed + */ + public function current() + { + if ($this->currentComplete == false) { + if ($this->loadData() === false) { + return false; + } + } + + return $this->currentData; + } + + /** + * Load from oci8 result + * + * @return bool + */ + protected function loadData() + { + $this->currentComplete = true; + $this->currentData = oci_fetch_assoc($this->resource); + + if ($this->currentData !== false) { + $this->position++; + return true; + } + return false; + } + + /** + * Next + */ + public function next() + { + return $this->loadData(); + } + + /** + * Key + * @return mixed + */ + public function key() + { + return $this->position; + } + + /** + * Rewind + */ + public function rewind() + { + if ($this->position > 0) { + throw new Exception\RuntimeException('Oci8 results cannot be rewound for multiple iterations'); + } + } + + /** + * Valid + * @return bool + */ + public function valid() + { + if ($this->currentComplete) { + return ($this->currentData !== false); + } + + return $this->loadData(); + } + + /** + * Count + * @return int + */ + public function count() + { + // @todo OCI8 row count in Driver Result + return null; + } + + /** + * @return int + */ + public function getFieldCount() + { + return oci_num_fields($this->resource); + } + + /** + * @return mixed|null + */ + public function getGeneratedValue() + { + // @todo OCI8 generated value in Driver Result + return null; + } +} diff --git a/library/Zend/Db/Adapter/Driver/Oci8/Statement.php b/library/Zend/Db/Adapter/Driver/Oci8/Statement.php new file mode 100755 index 0000000000..707442fdeb --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Oci8/Statement.php @@ -0,0 +1,311 @@ +driver = $driver; + return $this; + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return Statement + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Initialize + * + * @param resource $oci8 + * @return Statement + */ + public function initialize($oci8) + { + $this->oci8 = $oci8; + return $this; + } + + /** + * Set sql + * + * @param string $sql + * @return Statement + */ + public function setSql($sql) + { + $this->sql = $sql; + return $this; + } + + /** + * Set Parameter container + * + * @param ParameterContainer $parameterContainer + * @return Statement + */ + public function setParameterContainer(ParameterContainer $parameterContainer) + { + $this->parameterContainer = $parameterContainer; + return $this; + } + + /** + * Get resource + * + * @return mixed + */ + public function getResource() + { + return $this->resource; + } + + /** + * Set resource + * + * @param resource $oci8Statement + * @return Statement + */ + public function setResource($oci8Statement) + { + $type = oci_statement_type($oci8Statement); + if (false === $type || 'UNKNOWN' == $type) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid statement provided to %s', + __METHOD__ + )); + } + $this->resource = $oci8Statement; + $this->isPrepared = true; + return $this; + } + + /** + * Get sql + * + * @return string + */ + public function getSql() + { + return $this->sql; + } + + /** + * @return ParameterContainer + */ + public function getParameterContainer() + { + return $this->parameterContainer; + } + + /** + * @return bool + */ + public function isPrepared() + { + return $this->isPrepared; + } + + /** + * @param string $sql + * @return Statement + */ + public function prepare($sql = null) + { + if ($this->isPrepared) { + throw new Exception\RuntimeException('This statement has already been prepared'); + } + + $sql = ($sql) ?: $this->sql; + + // get oci8 statement resource + $this->resource = oci_parse($this->oci8, $sql); + + if (!$this->resource) { + $e = oci_error($this->oci8); + throw new Exception\InvalidQueryException( + 'Statement couldn\'t be produced with sql: ' . $sql, + null, + new Exception\ErrorException($e['message'], $e['code']) + ); + } + + $this->isPrepared = true; + return $this; + } + + /** + * Execute + * + * @param ParameterContainer $parameters + * @return mixed + */ + public function execute($parameters = null) + { + if (!$this->isPrepared) { + $this->prepare(); + } + + /** START Standard ParameterContainer Merging Block */ + if (!$this->parameterContainer instanceof ParameterContainer) { + if ($parameters instanceof ParameterContainer) { + $this->parameterContainer = $parameters; + $parameters = null; + } else { + $this->parameterContainer = new ParameterContainer(); + } + } + + if (is_array($parameters)) { + $this->parameterContainer->setFromArray($parameters); + } + + if ($this->parameterContainer->count() > 0) { + $this->bindParametersFromContainer(); + } + /** END Standard ParameterContainer Merging Block */ + + if ($this->profiler) { + $this->profiler->profilerStart($this); + } + + if ($this->driver->getConnection()->inTransaction()) { + $ret = @oci_execute($this->resource, OCI_NO_AUTO_COMMIT); + } else { + $ret = @oci_execute($this->resource, OCI_COMMIT_ON_SUCCESS); + } + + if ($this->profiler) { + $this->profiler->profilerFinish(); + } + + if ($ret === false) { + $e = oci_error($this->resource); + throw new Exception\RuntimeException($e['message'], $e['code']); + } + + $result = $this->driver->createResult($this->resource); + return $result; + } + + /** + * Bind parameters from container + * + * @param ParameterContainer $pContainer + */ + protected function bindParametersFromContainer() + { + $parameters = $this->parameterContainer->getNamedArray(); + + foreach ($parameters as $name => &$value) { + if ($this->parameterContainer->offsetHasErrata($name)) { + switch ($this->parameterContainer->offsetGetErrata($name)) { + case ParameterContainer::TYPE_NULL: + $type = null; + $value = null; + break; + case ParameterContainer::TYPE_DOUBLE: + case ParameterContainer::TYPE_INTEGER: + $type = SQLT_INT; + if (is_string($value)) { + $value = (int) $value; + } + break; + case ParameterContainer::TYPE_BINARY: + $type = SQLT_BIN; + break; + case ParameterContainer::TYPE_LOB: + $type = OCI_B_CLOB; + $clob = oci_new_descriptor($this->driver->getConnection()->getResource(), OCI_DTYPE_LOB); + $clob->writetemporary($value, OCI_TEMP_CLOB); + $value = $clob; + break; + case ParameterContainer::TYPE_STRING: + default: + $type = SQLT_CHR; + break; + } + } else { + $type = SQLT_CHR; + } + + oci_bind_by_name($this->resource, $name, $value, -1, $type); + } + } +} diff --git a/library/Zend/Db/Adapter/Driver/Pdo/Connection.php b/library/Zend/Db/Adapter/Driver/Pdo/Connection.php new file mode 100755 index 0000000000..1cd2c66675 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Pdo/Connection.php @@ -0,0 +1,488 @@ +setConnectionParameters($connectionParameters); + } elseif ($connectionParameters instanceof \PDO) { + $this->setResource($connectionParameters); + } elseif (null !== $connectionParameters) { + throw new Exception\InvalidArgumentException('$connection must be an array of parameters, a PDO object or null'); + } + } + + /** + * Set driver + * + * @param Pdo $driver + * @return Connection + */ + public function setDriver(Pdo $driver) + { + $this->driver = $driver; + return $this; + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return Connection + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Get driver name + * + * @return null|string + */ + public function getDriverName() + { + return $this->driverName; + } + + /** + * Set connection parameters + * + * @param array $connectionParameters + * @return void + */ + public function setConnectionParameters(array $connectionParameters) + { + $this->connectionParameters = $connectionParameters; + if (isset($connectionParameters['dsn'])) { + $this->driverName = substr($connectionParameters['dsn'], 0, + strpos($connectionParameters['dsn'], ':') + ); + } elseif (isset($connectionParameters['pdodriver'])) { + $this->driverName = strtolower($connectionParameters['pdodriver']); + } elseif (isset($connectionParameters['driver'])) { + $this->driverName = strtolower(substr( + str_replace(array('-', '_', ' '), '', $connectionParameters['driver']), + 3 + )); + } + } + + /** + * Get connection parameters + * + * @return array + */ + public function getConnectionParameters() + { + return $this->connectionParameters; + } + + /** + * Get the dsn string for this connection + * @throws \Zend\Db\Adapter\Exception\RunTimeException + * @return string + */ + public function getDsn() + { + if (!$this->dsn) { + throw new Exception\RunTimeException("The DSN has not been set or constructed from parameters in connect() for this Connection"); + } + + return $this->dsn; + } + + /** + * Get current schema + * + * @return string + */ + public function getCurrentSchema() + { + if (!$this->isConnected()) { + $this->connect(); + } + + switch ($this->driverName) { + case 'mysql': + $sql = 'SELECT DATABASE()'; + break; + case 'sqlite': + return 'main'; + case 'sqlsrv': + case 'dblib': + $sql = 'SELECT SCHEMA_NAME()'; + break; + case 'pgsql': + default: + $sql = 'SELECT CURRENT_SCHEMA'; + break; + } + + /** @var $result \PDOStatement */ + $result = $this->resource->query($sql); + if ($result instanceof \PDOStatement) { + return $result->fetchColumn(); + } + return false; + } + + /** + * Set resource + * + * @param \PDO $resource + * @return Connection + */ + public function setResource(\PDO $resource) + { + $this->resource = $resource; + $this->driverName = strtolower($this->resource->getAttribute(\PDO::ATTR_DRIVER_NAME)); + return $this; + } + + /** + * Get resource + * + * @return \PDO + */ + public function getResource() + { + if (!$this->isConnected()) { + $this->connect(); + } + return $this->resource; + } + + /** + * Connect + * + * @return Connection + * @throws Exception\InvalidConnectionParametersException + * @throws Exception\RuntimeException + */ + public function connect() + { + if ($this->resource) { + return $this; + } + + $dsn = $username = $password = $hostname = $database = null; + $options = array(); + foreach ($this->connectionParameters as $key => $value) { + switch (strtolower($key)) { + case 'dsn': + $dsn = $value; + break; + case 'driver': + $value = strtolower($value); + if (strpos($value, 'pdo') === 0) { + $pdoDriver = strtolower(substr(str_replace(array('-', '_', ' '), '', $value), 3)); + } + break; + case 'pdodriver': + $pdoDriver = (string) $value; + break; + case 'user': + case 'username': + $username = (string) $value; + break; + case 'pass': + case 'password': + $password = (string) $value; + break; + case 'host': + case 'hostname': + $hostname = (string) $value; + break; + case 'port': + $port = (int) $value; + break; + case 'database': + case 'dbname': + $database = (string) $value; + break; + case 'charset': + $charset = (string) $value; + break; + case 'driver_options': + case 'options': + $value = (array) $value; + $options = array_diff_key($options, $value) + $value; + break; + default: + $options[$key] = $value; + break; + } + } + + if (!isset($dsn) && isset($pdoDriver)) { + $dsn = array(); + switch ($pdoDriver) { + case 'sqlite': + $dsn[] = $database; + break; + case 'sqlsrv': + if (isset($database)) { + $dsn[] = "database={$database}"; + } + if (isset($hostname)) { + $dsn[] = "server={$hostname}"; + } + break; + default: + if (isset($database)) { + $dsn[] = "dbname={$database}"; + } + if (isset($hostname)) { + $dsn[] = "host={$hostname}"; + } + if (isset($port)) { + $dsn[] = "port={$port}"; + } + if (isset($charset) && $pdoDriver != 'pgsql') { + $dsn[] = "charset={$charset}"; + } + break; + } + $dsn = $pdoDriver . ':' . implode(';', $dsn); + } elseif (!isset($dsn)) { + throw new Exception\InvalidConnectionParametersException( + 'A dsn was not provided or could not be constructed from your parameters', + $this->connectionParameters + ); + } + + $this->dsn = $dsn; + + try { + $this->resource = new \PDO($dsn, $username, $password, $options); + $this->resource->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + if (isset($charset) && $pdoDriver == 'pgsql') { + $this->resource->exec('SET NAMES ' . $this->resource->quote($charset)); + } + $this->driverName = strtolower($this->resource->getAttribute(\PDO::ATTR_DRIVER_NAME)); + } catch (\PDOException $e) { + $code = $e->getCode(); + if (!is_long($code)) { + $code = null; + } + throw new Exception\RuntimeException('Connect Error: ' . $e->getMessage(), $code, $e); + } + + return $this; + } + + /** + * Is connected + * + * @return bool + */ + public function isConnected() + { + return ($this->resource instanceof \PDO); + } + + /** + * Disconnect + * + * @return Connection + */ + public function disconnect() + { + if ($this->isConnected()) { + $this->resource = null; + } + return $this; + } + + /** + * Begin transaction + * + * @return Connection + */ + public function beginTransaction() + { + if (!$this->isConnected()) { + $this->connect(); + } + $this->resource->beginTransaction(); + $this->inTransaction = true; + return $this; + } + + /** + * In transaction + * + * @return bool + */ + public function inTransaction() + { + return $this->inTransaction; + } + + /** + * Commit + * + * @return Connection + */ + public function commit() + { + if (!$this->isConnected()) { + $this->connect(); + } + + $this->resource->commit(); + $this->inTransaction = false; + return $this; + } + + /** + * Rollback + * + * @return Connection + * @throws Exception\RuntimeException + */ + public function rollback() + { + if (!$this->isConnected()) { + throw new Exception\RuntimeException('Must be connected before you can rollback'); + } + + if (!$this->inTransaction) { + throw new Exception\RuntimeException('Must call beginTransaction() before you can rollback'); + } + + $this->resource->rollBack(); + return $this; + } + + /** + * Execute + * + * @param $sql + * @return Result + * @throws Exception\InvalidQueryException + */ + public function execute($sql) + { + if (!$this->isConnected()) { + $this->connect(); + } + + if ($this->profiler) { + $this->profiler->profilerStart($sql); + } + + $resultResource = $this->resource->query($sql); + + if ($this->profiler) { + $this->profiler->profilerFinish($sql); + } + + if ($resultResource === false) { + $errorInfo = $this->resource->errorInfo(); + throw new Exception\InvalidQueryException($errorInfo[2]); + } + + $result = $this->driver->createResult($resultResource, $sql); + return $result; + } + + /** + * Prepare + * + * @param string $sql + * @return Statement + */ + public function prepare($sql) + { + if (!$this->isConnected()) { + $this->connect(); + } + + $statement = $this->driver->createStatement($sql); + return $statement; + } + + /** + * Get last generated id + * + * @param string $name + * @return string|null|false + */ + public function getLastGeneratedValue($name = null) + { + if ($name === null && $this->driverName == 'pgsql') { + return null; + } + + try { + return $this->resource->lastInsertId($name); + } catch (\Exception $e) { + // do nothing + } + return false; + } +} diff --git a/library/Zend/Db/Adapter/Driver/Pdo/Feature/OracleRowCounter.php b/library/Zend/Db/Adapter/Driver/Pdo/Feature/OracleRowCounter.php new file mode 100755 index 0000000000..2a25cdd6bf --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Pdo/Feature/OracleRowCounter.php @@ -0,0 +1,78 @@ +getSql(); + if ($sql == '' || stripos($sql, 'select') === false) { + return null; + } + $countSql = 'SELECT COUNT(*) as "count" FROM (' . $sql . ')'; + $countStmt->prepare($countSql); + $result = $countStmt->execute(); + $countRow = $result->getResource()->fetch(\PDO::FETCH_ASSOC); + unset($statement, $result); + return $countRow['count']; + } + + /** + * @param $sql + * @return null|int + */ + public function getCountForSql($sql) + { + if (stripos($sql, 'select') === false) { + return null; + } + $countSql = 'SELECT COUNT(*) as count FROM (' . $sql . ')'; + /** @var $pdo \PDO */ + $pdo = $this->driver->getConnection()->getResource(); + $result = $pdo->query($countSql); + $countRow = $result->fetch(\PDO::FETCH_ASSOC); + return $countRow['count']; + } + + /** + * @param $context + * @return \Closure + */ + public function getRowCountClosure($context) + { + $oracleRowCounter = $this; + return function () use ($oracleRowCounter, $context) { + /** @var $oracleRowCounter OracleRowCounter */ + return ($context instanceof Pdo\Statement) + ? $oracleRowCounter->getCountForStatement($context) + : $oracleRowCounter->getCountForSql($context); + }; + } +} diff --git a/library/Zend/Db/Adapter/Driver/Pdo/Feature/SqliteRowCounter.php b/library/Zend/Db/Adapter/Driver/Pdo/Feature/SqliteRowCounter.php new file mode 100755 index 0000000000..13c8d66d41 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Pdo/Feature/SqliteRowCounter.php @@ -0,0 +1,78 @@ +getSql(); + if ($sql == '' || stripos($sql, 'select') === false) { + return null; + } + $countSql = 'SELECT COUNT(*) as "count" FROM (' . $sql . ')'; + $countStmt->prepare($countSql); + $result = $countStmt->execute(); + $countRow = $result->getResource()->fetch(\PDO::FETCH_ASSOC); + unset($statement, $result); + return $countRow['count']; + } + + /** + * @param $sql + * @return null|int + */ + public function getCountForSql($sql) + { + if (stripos($sql, 'select') === false) { + return null; + } + $countSql = 'SELECT COUNT(*) as count FROM (' . $sql . ')'; + /** @var $pdo \PDO */ + $pdo = $this->driver->getConnection()->getResource(); + $result = $pdo->query($countSql); + $countRow = $result->fetch(\PDO::FETCH_ASSOC); + return $countRow['count']; + } + + /** + * @param $context + * @return \Closure + */ + public function getRowCountClosure($context) + { + $sqliteRowCounter = $this; + return function () use ($sqliteRowCounter, $context) { + /** @var $sqliteRowCounter SqliteRowCounter */ + return ($context instanceof Pdo\Statement) + ? $sqliteRowCounter->getCountForStatement($context) + : $sqliteRowCounter->getCountForSql($context); + }; + } +} diff --git a/library/Zend/Db/Adapter/Driver/Pdo/Pdo.php b/library/Zend/Db/Adapter/Driver/Pdo/Pdo.php new file mode 100755 index 0000000000..3de7beb498 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Pdo/Pdo.php @@ -0,0 +1,314 @@ +registerConnection($connection); + $this->registerStatementPrototype(($statementPrototype) ?: new Statement()); + $this->registerResultPrototype(($resultPrototype) ?: new Result()); + if (is_array($features)) { + foreach ($features as $name => $feature) { + $this->addFeature($name, $feature); + } + } elseif ($features instanceof AbstractFeature) { + $this->addFeature($features->getName(), $features); + } elseif ($features === self::FEATURES_DEFAULT) { + $this->setupDefaultFeatures(); + } + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return Pdo + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + if ($this->connection instanceof Profiler\ProfilerAwareInterface) { + $this->connection->setProfiler($profiler); + } + if ($this->statementPrototype instanceof Profiler\ProfilerAwareInterface) { + $this->statementPrototype->setProfiler($profiler); + } + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Register connection + * + * @param Connection $connection + * @return Pdo + */ + public function registerConnection(Connection $connection) + { + $this->connection = $connection; + $this->connection->setDriver($this); + return $this; + } + + /** + * Register statement prototype + * + * @param Statement $statementPrototype + */ + public function registerStatementPrototype(Statement $statementPrototype) + { + $this->statementPrototype = $statementPrototype; + $this->statementPrototype->setDriver($this); + } + + /** + * Register result prototype + * + * @param Result $resultPrototype + */ + public function registerResultPrototype(Result $resultPrototype) + { + $this->resultPrototype = $resultPrototype; + } + + /** + * Add feature + * + * @param string $name + * @param AbstractFeature $feature + * @return Pdo + */ + public function addFeature($name, $feature) + { + if ($feature instanceof AbstractFeature) { + $name = $feature->getName(); // overwrite the name, just in case + $feature->setDriver($this); + } + $this->features[$name] = $feature; + return $this; + } + + /** + * Setup the default features for Pdo + * + * @return Pdo + */ + public function setupDefaultFeatures() + { + $driverName = $this->connection->getDriverName(); + if ($driverName == 'sqlite') { + $this->addFeature(null, new Feature\SqliteRowCounter); + } elseif ($driverName == 'oci') { + $this->addFeature(null, new Feature\OracleRowCounter); + } + return $this; + } + + /** + * Get feature + * + * @param $name + * @return AbstractFeature|false + */ + public function getFeature($name) + { + if (isset($this->features[$name])) { + return $this->features[$name]; + } + return false; + } + + /** + * Get database platform name + * + * @param string $nameFormat + * @return string + */ + public function getDatabasePlatformName($nameFormat = self::NAME_FORMAT_CAMELCASE) + { + $name = $this->getConnection()->getDriverName(); + if ($nameFormat == self::NAME_FORMAT_CAMELCASE) { + switch ($name) { + case 'pgsql': + return 'Postgresql'; + case 'oci': + return 'Oracle'; + case 'dblib': + case 'sqlsrv': + return 'SqlServer'; + default: + return ucfirst($name); + } + } else { + switch ($name) { + case 'sqlite': + return 'SQLite'; + case 'mysql': + return 'MySQL'; + case 'pgsql': + return 'PostgreSQL'; + case 'oci': + return 'Oracle'; + case 'dblib': + case 'sqlsrv': + return 'SQLServer'; + default: + return ucfirst($name); + } + } + } + + /** + * Check environment + */ + public function checkEnvironment() + { + if (!extension_loaded('PDO')) { + throw new Exception\RuntimeException('The PDO extension is required for this adapter but the extension is not loaded'); + } + } + + /** + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * @param string|PDOStatement $sqlOrResource + * @return Statement + */ + public function createStatement($sqlOrResource = null) + { + $statement = clone $this->statementPrototype; + if ($sqlOrResource instanceof PDOStatement) { + $statement->setResource($sqlOrResource); + } else { + if (is_string($sqlOrResource)) { + $statement->setSql($sqlOrResource); + } + if (!$this->connection->isConnected()) { + $this->connection->connect(); + } + $statement->initialize($this->connection->getResource()); + } + return $statement; + } + + /** + * @param resource $resource + * @param mixed $context + * @return Result + */ + public function createResult($resource, $context = null) + { + $result = clone $this->resultPrototype; + $rowCount = null; + + // special feature, sqlite PDO counter + if ($this->connection->getDriverName() == 'sqlite' + && ($sqliteRowCounter = $this->getFeature('SqliteRowCounter')) + && $resource->columnCount() > 0) { + $rowCount = $sqliteRowCounter->getRowCountClosure($context); + } + + // special feature, oracle PDO counter + if ($this->connection->getDriverName() == 'oci' + && ($oracleRowCounter = $this->getFeature('OracleRowCounter')) + && $resource->columnCount() > 0) { + $rowCount = $oracleRowCounter->getRowCountClosure($context); + } + + + $result->initialize($resource, $this->connection->getLastGeneratedValue(), $rowCount); + return $result; + } + + /** + * @return array + */ + public function getPrepareType() + { + return self::PARAMETERIZATION_NAMED; + } + + /** + * @param string $name + * @param string|null $type + * @return string + */ + public function formatParameterName($name, $type = null) + { + if ($type == null && !is_numeric($name) || $type == self::PARAMETERIZATION_NAMED) { + return ':' . $name; + } + + return '?'; + } + + /** + * @return mixed + */ + public function getLastGeneratedValue($name = null) + { + return $this->connection->getLastGeneratedValue($name); + } +} diff --git a/library/Zend/Db/Adapter/Driver/Pdo/Result.php b/library/Zend/Db/Adapter/Driver/Pdo/Result.php new file mode 100755 index 0000000000..9323282d6f --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Pdo/Result.php @@ -0,0 +1,226 @@ +resource = $resource; + $this->generatedValue = $generatedValue; + $this->rowCount = $rowCount; + + return $this; + } + + /** + * @return null + */ + public function buffer() + { + return null; + } + + /** + * @return bool|null + */ + public function isBuffered() + { + return false; + } + + /** + * Get resource + * + * @return mixed + */ + public function getResource() + { + return $this->resource; + } + + /** + * Get the data + * @return array + */ + public function current() + { + if ($this->currentComplete) { + return $this->currentData; + } + + $this->currentData = $this->resource->fetch(\PDO::FETCH_ASSOC); + $this->currentComplete = true; + return $this->currentData; + } + + /** + * Next + * + * @return mixed + */ + public function next() + { + $this->currentData = $this->resource->fetch(\PDO::FETCH_ASSOC); + $this->currentComplete = true; + $this->position++; + return $this->currentData; + } + + /** + * Key + * + * @return mixed + */ + public function key() + { + return $this->position; + } + + /** + * @throws Exception\RuntimeException + * @return void + */ + public function rewind() + { + if ($this->statementMode == self::STATEMENT_MODE_FORWARD && $this->position > 0) { + throw new Exception\RuntimeException( + 'This result is a forward only result set, calling rewind() after moving forward is not supported' + ); + } + $this->currentData = $this->resource->fetch(\PDO::FETCH_ASSOC); + $this->currentComplete = true; + $this->position = 0; + } + + /** + * Valid + * + * @return bool + */ + public function valid() + { + return ($this->currentData !== false); + } + + /** + * Count + * + * @return int + */ + public function count() + { + if (is_int($this->rowCount)) { + return $this->rowCount; + } + if ($this->rowCount instanceof \Closure) { + $this->rowCount = (int) call_user_func($this->rowCount); + } else { + $this->rowCount = (int) $this->resource->rowCount(); + } + return $this->rowCount; + } + + /** + * @return int + */ + public function getFieldCount() + { + return $this->resource->columnCount(); + } + + /** + * Is query result + * + * @return bool + */ + public function isQueryResult() + { + return ($this->resource->columnCount() > 0); + } + + /** + * Get affected rows + * + * @return int + */ + public function getAffectedRows() + { + return $this->resource->rowCount(); + } + + /** + * @return mixed|null + */ + public function getGeneratedValue() + { + return $this->generatedValue; + } +} diff --git a/library/Zend/Db/Adapter/Driver/Pdo/Statement.php b/library/Zend/Db/Adapter/Driver/Pdo/Statement.php new file mode 100755 index 0000000000..891bec9d7e --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Pdo/Statement.php @@ -0,0 +1,310 @@ +driver = $driver; + return $this; + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return Statement + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Initialize + * + * @param \PDO $connectionResource + * @return Statement + */ + public function initialize(\PDO $connectionResource) + { + $this->pdo = $connectionResource; + return $this; + } + + /** + * Set resource + * + * @param \PDOStatement $pdoStatement + * @return Statement + */ + public function setResource(\PDOStatement $pdoStatement) + { + $this->resource = $pdoStatement; + return $this; + } + + /** + * Get resource + * + * @return mixed + */ + public function getResource() + { + return $this->resource; + } + + /** + * Set sql + * + * @param string $sql + * @return Statement + */ + public function setSql($sql) + { + $this->sql = $sql; + return $this; + } + + /** + * Get sql + * + * @return string + */ + public function getSql() + { + return $this->sql; + } + + /** + * @param ParameterContainer $parameterContainer + * @return Statement + */ + public function setParameterContainer(ParameterContainer $parameterContainer) + { + $this->parameterContainer = $parameterContainer; + return $this; + } + + /** + * @return ParameterContainer + */ + public function getParameterContainer() + { + return $this->parameterContainer; + } + + /** + * @param string $sql + * @throws Exception\RuntimeException + */ + public function prepare($sql = null) + { + if ($this->isPrepared) { + throw new Exception\RuntimeException('This statement has been prepared already'); + } + + if ($sql == null) { + $sql = $this->sql; + } + + $this->resource = $this->pdo->prepare($sql); + + if ($this->resource === false) { + $error = $this->pdo->errorInfo(); + throw new Exception\RuntimeException($error[2]); + } + + $this->isPrepared = true; + } + + /** + * @return bool + */ + public function isPrepared() + { + return $this->isPrepared; + } + + /** + * @param mixed $parameters + * @throws Exception\InvalidQueryException + * @return Result + */ + public function execute($parameters = null) + { + if (!$this->isPrepared) { + $this->prepare(); + } + + /** START Standard ParameterContainer Merging Block */ + if (!$this->parameterContainer instanceof ParameterContainer) { + if ($parameters instanceof ParameterContainer) { + $this->parameterContainer = $parameters; + $parameters = null; + } else { + $this->parameterContainer = new ParameterContainer(); + } + } + + if (is_array($parameters)) { + $this->parameterContainer->setFromArray($parameters); + } + + if ($this->parameterContainer->count() > 0) { + $this->bindParametersFromContainer(); + } + /** END Standard ParameterContainer Merging Block */ + + if ($this->profiler) { + $this->profiler->profilerStart($this); + } + + try { + $this->resource->execute(); + } catch (\PDOException $e) { + if ($this->profiler) { + $this->profiler->profilerFinish(); + } + throw new Exception\InvalidQueryException( + 'Statement could not be executed (' . implode(' - ', $this->resource->errorInfo()) . ')', + null, + $e + ); + } + + if ($this->profiler) { + $this->profiler->profilerFinish(); + } + + $result = $this->driver->createResult($this->resource, $this); + return $result; + } + + /** + * Bind parameters from container + */ + protected function bindParametersFromContainer() + { + if ($this->parametersBound) { + return; + } + + $parameters = $this->parameterContainer->getNamedArray(); + foreach ($parameters as $name => &$value) { + if (is_bool($value)) { + $type = \PDO::PARAM_BOOL; + } elseif (is_int($value)) { + $type = \PDO::PARAM_INT; + } else { + $type = \PDO::PARAM_STR; + } + if ($this->parameterContainer->offsetHasErrata($name)) { + switch ($this->parameterContainer->offsetGetErrata($name)) { + case ParameterContainer::TYPE_INTEGER: + $type = \PDO::PARAM_INT; + break; + case ParameterContainer::TYPE_NULL: + $type = \PDO::PARAM_NULL; + break; + case ParameterContainer::TYPE_LOB: + $type = \PDO::PARAM_LOB; + break; + } + } + + // parameter is named or positional, value is reference + $parameter = is_int($name) ? ($name + 1) : $name; + $this->resource->bindParam($parameter, $value, $type); + } + } + + /** + * Perform a deep clone + * @return Statement A cloned statement + */ + public function __clone() + { + $this->isPrepared = false; + $this->parametersBound = false; + $this->resource = null; + if ($this->parameterContainer) { + $this->parameterContainer = clone $this->parameterContainer; + } + } +} diff --git a/library/Zend/Db/Adapter/Driver/Pgsql/Connection.php b/library/Zend/Db/Adapter/Driver/Pgsql/Connection.php new file mode 100755 index 0000000000..fa91289a43 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Pgsql/Connection.php @@ -0,0 +1,311 @@ +setConnectionParameters($connectionInfo); + } elseif (is_resource($connectionInfo)) { + $this->setResource($connectionInfo); + } + } + + /** + * Set connection parameters + * + * @param array $connectionParameters + * @return Connection + */ + public function setConnectionParameters(array $connectionParameters) + { + $this->connectionParameters = $connectionParameters; + return $this; + } + + /** + * Set driver + * + * @param Pgsql $driver + * @return Connection + */ + public function setDriver(Pgsql $driver) + { + $this->driver = $driver; + return $this; + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return Connection + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Set resource + * + * @param resource $resource + * @return Connection + */ + public function setResource($resource) + { + $this->resource = $resource; + return; + } + + /** + * Get current schema + * + * @return null|string + */ + public function getCurrentSchema() + { + if (!$this->isConnected()) { + $this->connect(); + } + + $result = pg_query($this->resource, 'SELECT CURRENT_SCHEMA AS "currentschema"'); + if ($result == false) { + return null; + } + return pg_fetch_result($result, 0, 'currentschema'); + } + + /** + * Get resource + * + * @return resource + */ + public function getResource() + { + if (!$this->isConnected()) { + $this->connect(); + } + return $this->resource; + } + + /** + * Connect to the database + * + * @return Connection + * @throws Exception\RuntimeException on failure + */ + public function connect() + { + if (is_resource($this->resource)) { + return $this; + } + + // localize + $p = $this->connectionParameters; + + // given a list of key names, test for existence in $p + $findParameterValue = function (array $names) use ($p) { + foreach ($names as $name) { + if (isset($p[$name])) { + return $p[$name]; + } + } + return null; + }; + + $connection = array(); + $connection['host'] = $findParameterValue(array('hostname', 'host')); + $connection['user'] = $findParameterValue(array('username', 'user')); + $connection['password'] = $findParameterValue(array('password', 'passwd', 'pw')); + $connection['dbname'] = $findParameterValue(array('database', 'dbname', 'db', 'schema')); + $connection['port'] = (isset($p['port'])) ? (int) $p['port'] : null; + $connection['socket'] = (isset($p['socket'])) ? $p['socket'] : null; + + $connection = array_filter($connection); // remove nulls + $connection = http_build_query($connection, null, ' '); // @link http://php.net/pg_connect + + set_error_handler(function ($number, $string) { + throw new Exception\RuntimeException( + __METHOD__ . ': Unable to connect to database', null, new Exception\ErrorException($string, $number) + ); + }); + $this->resource = pg_connect($connection); + restore_error_handler(); + + if ($this->resource === false) { + throw new Exception\RuntimeException(sprintf( + '%s: Unable to connect to database', + __METHOD__ + )); + } + + return $this; + } + + /** + * @return bool + */ + public function isConnected() + { + return (is_resource($this->resource)); + } + + /** + * @return void + */ + public function disconnect() + { + pg_close($this->resource); + } + + /** + * @return void + */ + public function beginTransaction() + { + if ($this->inTransaction) { + throw new Exception\RuntimeException('Nested transactions are not supported'); + } + + if (!$this->isConnected()) { + $this->connect(); + } + + pg_query($this->resource, 'BEGIN'); + $this->inTransaction = true; + } + + /** + * In transaction + * + * @return bool + */ + public function inTransaction() + { + return $this->inTransaction; + } + + /** + * @return void + */ + public function commit() + { + if (!$this->inTransaction) { + return; // We ignore attempts to commit non-existing transaction + } + + pg_query($this->resource, 'COMMIT'); + $this->inTransaction = false; + } + + /** + * @return void + */ + public function rollback() + { + if (!$this->inTransaction) { + return; + } + + pg_query($this->resource, 'ROLLBACK'); + $this->inTransaction = false; + } + + /** + * @param string $sql + * @throws Exception\InvalidQueryException + * @return resource|\Zend\Db\ResultSet\ResultSetInterface + */ + public function execute($sql) + { + if (!$this->isConnected()) { + $this->connect(); + } + + if ($this->profiler) { + $this->profiler->profilerStart($sql); + } + + $resultResource = pg_query($this->resource, $sql); + + if ($this->profiler) { + $this->profiler->profilerFinish($sql); + } + + // if the returnValue is something other than a pg result resource, bypass wrapping it + if ($resultResource === false) { + throw new Exception\InvalidQueryException(pg_errormessage()); + } + + $resultPrototype = $this->driver->createResult(($resultResource === true) ? $this->resource : $resultResource); + return $resultPrototype; + } + + /** + * @param null $name Ignored + * @return string + */ + public function getLastGeneratedValue($name = null) + { + if ($name == null) { + return null; + } + $result = pg_query($this->resource, 'SELECT CURRVAL(\'' . str_replace('\'', '\\\'', $name) . '\') as "currval"'); + return pg_fetch_result($result, 0, 'currval'); + } +} diff --git a/library/Zend/Db/Adapter/Driver/Pgsql/Pgsql.php b/library/Zend/Db/Adapter/Driver/Pgsql/Pgsql.php new file mode 100755 index 0000000000..36e5e0f294 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Pgsql/Pgsql.php @@ -0,0 +1,227 @@ + false + ); + + /** + * Constructor + * + * @param array|Connection|resource $connection + * @param null|Statement $statementPrototype + * @param null|Result $resultPrototype + * @param array $options + */ + public function __construct($connection, Statement $statementPrototype = null, Result $resultPrototype = null, $options = null) + { + if (!$connection instanceof Connection) { + $connection = new Connection($connection); + } + + $this->registerConnection($connection); + $this->registerStatementPrototype(($statementPrototype) ?: new Statement()); + $this->registerResultPrototype(($resultPrototype) ?: new Result()); + } + + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + if ($this->connection instanceof Profiler\ProfilerAwareInterface) { + $this->connection->setProfiler($profiler); + } + if ($this->statementPrototype instanceof Profiler\ProfilerAwareInterface) { + $this->statementPrototype->setProfiler($profiler); + } + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Register connection + * + * @param Connection $connection + * @return Pgsql + */ + public function registerConnection(Connection $connection) + { + $this->connection = $connection; + $this->connection->setDriver($this); + return $this; + } + + /** + * Register statement prototype + * + * @param Statement $statement + * @return Pgsql + */ + public function registerStatementPrototype(Statement $statement) + { + $this->statementPrototype = $statement; + $this->statementPrototype->setDriver($this); // needs access to driver to createResult() + return $this; + } + + /** + * Register result prototype + * + * @param Result $result + * @return Pgsql + */ + public function registerResultPrototype(Result $result) + { + $this->resultPrototype = $result; + return $this; + } + + /** + * Get database platform name + * + * @param string $nameFormat + * @return string + */ + public function getDatabasePlatformName($nameFormat = self::NAME_FORMAT_CAMELCASE) + { + if ($nameFormat == self::NAME_FORMAT_CAMELCASE) { + return 'Postgresql'; + } + + return 'PostgreSQL'; + } + + /** + * Check environment + * + * @throws Exception\RuntimeException + * @return bool + */ + public function checkEnvironment() + { + if (!extension_loaded('pgsql')) { + throw new Exception\RuntimeException('The PostgreSQL (pgsql) extension is required for this adapter but the extension is not loaded'); + } + } + + /** + * Get connection + * + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * Create statement + * + * @param string|null $sqlOrResource + * @return Statement + */ + public function createStatement($sqlOrResource = null) + { + $statement = clone $this->statementPrototype; + + if (is_string($sqlOrResource)) { + $statement->setSql($sqlOrResource); + } + + if (!$this->connection->isConnected()) { + $this->connection->connect(); + } + + $statement->initialize($this->connection->getResource()); + return $statement; + } + + /** + * Create result + * + * @param resource $resource + * @return Result + */ + public function createResult($resource) + { + $result = clone $this->resultPrototype; + $result->initialize($resource, $this->connection->getLastGeneratedValue()); + return $result; + } + + /** + * Get prepare Type + * + * @return array + */ + public function getPrepareType() + { + return self::PARAMETERIZATION_POSITIONAL; + } + + /** + * Format parameter name + * + * @param string $name + * @param mixed $type + * @return string + */ + public function formatParameterName($name, $type = null) + { + return '$#'; + } + + /** + * Get last generated value + * + * @param string $name + * @return mixed + */ + public function getLastGeneratedValue($name = null) + { + return $this->connection->getLastGeneratedValue($name); + } +} diff --git a/library/Zend/Db/Adapter/Driver/Pgsql/Result.php b/library/Zend/Db/Adapter/Driver/Pgsql/Result.php new file mode 100755 index 0000000000..6c2410dae8 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Pgsql/Result.php @@ -0,0 +1,192 @@ +resource = $resource; + $this->count = pg_num_rows($this->resource); + $this->generatedValue = $generatedValue; + } + + /** + * Current + * + * @return array|bool|mixed + */ + public function current() + { + if ($this->count === 0) { + return false; + } + return pg_fetch_assoc($this->resource, $this->position); + } + + /** + * Next + * + * @return void + */ + public function next() + { + $this->position++; + } + + /** + * Key + * + * @return int|mixed + */ + public function key() + { + return $this->position; + } + + /** + * Valid + * + * @return bool + */ + public function valid() + { + return ($this->position < $this->count); + } + + /** + * Rewind + * + * @return void + */ + public function rewind() + { + $this->position = 0; + } + + /** + * Buffer + * + * @return null + */ + public function buffer() + { + return null; + } + + /** + * Is buffered + * + * @return false + */ + public function isBuffered() + { + return false; + } + + /** + * Is query result + * + * @return bool + */ + public function isQueryResult() + { + return (pg_num_fields($this->resource) > 0); + } + + /** + * Get affected rows + * + * @return int + */ + public function getAffectedRows() + { + return pg_affected_rows($this->resource); + } + + /** + * Get generated value + * + * @return mixed|null + */ + public function getGeneratedValue() + { + return $this->generatedValue; + } + + /** + * Get resource + */ + public function getResource() + { + // TODO: Implement getResource() method. + } + + /** + * Count + * + * (PHP 5 >= 5.1.0)
+ * Count elements of an object + * @link http://php.net/manual/en/countable.count.php + * @return int The custom count as an integer. + *

+ *

+ * The return value is cast to an integer. + */ + public function count() + { + return $this->count; + } + + /** + * Get field count + * + * @return int + */ + public function getFieldCount() + { + return pg_num_fields($this->resource); + } +} diff --git a/library/Zend/Db/Adapter/Driver/Pgsql/Statement.php b/library/Zend/Db/Adapter/Driver/Pgsql/Statement.php new file mode 100755 index 0000000000..c105a6647e --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Pgsql/Statement.php @@ -0,0 +1,241 @@ +driver = $driver; + return $this; + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return Statement + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Initialize + * + * @param resource $pgsql + * @return void + * @throws Exception\RuntimeException for invalid or missing postgresql connection + */ + public function initialize($pgsql) + { + if (!is_resource($pgsql) || get_resource_type($pgsql) !== 'pgsql link') { + throw new Exception\RuntimeException(sprintf( + '%s: Invalid or missing postgresql connection; received "%s"', + __METHOD__, + get_resource_type($pgsql) + )); + } + $this->pgsql = $pgsql; + } + + /** + * Get resource + * + * @return resource + */ + public function getResource() + { + // TODO: Implement getResource() method. + } + + /** + * Set sql + * + * @param string $sql + * @return Statement + */ + public function setSql($sql) + { + $this->sql = $sql; + return $this; + } + + /** + * Get sql + * + * @return string + */ + public function getSql() + { + return $this->sql; + } + + /** + * Set parameter container + * + * @param ParameterContainer $parameterContainer + * @return Statement + */ + public function setParameterContainer(ParameterContainer $parameterContainer) + { + $this->parameterContainer = $parameterContainer; + return $this; + } + + /** + * Get parameter container + * + * @return ParameterContainer + */ + public function getParameterContainer() + { + return $this->parameterContainer; + } + + /** + * Prepare + * + * @param string $sql + */ + public function prepare($sql = null) + { + $sql = ($sql) ?: $this->sql; + + $pCount = 1; + $sql = preg_replace_callback( + '#\$\##', function ($foo) use (&$pCount) { + return '$' . $pCount++; + }, + $sql + ); + + $this->sql = $sql; + $this->statementName = 'statement' . ++static::$statementIndex; + $this->resource = pg_prepare($this->pgsql, $this->statementName, $sql); + } + + /** + * Is prepared + * + * @return bool + */ + public function isPrepared() + { + return isset($this->resource); + } + + /** + * Execute + * + * @param ParameterContainer|null $parameters + * @throws Exception\InvalidQueryException + * @return Result + */ + public function execute($parameters = null) + { + if (!$this->isPrepared()) { + $this->prepare(); + } + + /** START Standard ParameterContainer Merging Block */ + if (!$this->parameterContainer instanceof ParameterContainer) { + if ($parameters instanceof ParameterContainer) { + $this->parameterContainer = $parameters; + $parameters = null; + } else { + $this->parameterContainer = new ParameterContainer(); + } + } + + if (is_array($parameters)) { + $this->parameterContainer->setFromArray($parameters); + } + + if ($this->parameterContainer->count() > 0) { + $parameters = $this->parameterContainer->getPositionalArray(); + } + /** END Standard ParameterContainer Merging Block */ + + if ($this->profiler) { + $this->profiler->profilerStart($this); + } + + $resultResource = pg_execute($this->pgsql, $this->statementName, (array) $parameters); + + if ($this->profiler) { + $this->profiler->profilerFinish(); + } + + if ($resultResource === false) { + throw new Exception\InvalidQueryException(pg_last_error()); + } + + $result = $this->driver->createResult($resultResource); + return $result; + } +} diff --git a/library/Zend/Db/Adapter/Driver/ResultInterface.php b/library/Zend/Db/Adapter/Driver/ResultInterface.php new file mode 100755 index 0000000000..cb1f407849 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/ResultInterface.php @@ -0,0 +1,67 @@ +setConnectionParameters($connectionInfo); + } elseif (is_resource($connectionInfo)) { + $this->setResource($connectionInfo); + } else { + throw new Exception\InvalidArgumentException('$connection must be an array of parameters or a resource'); + } + } + + /** + * Set driver + * + * @param Sqlsrv $driver + * @return Connection + */ + public function setDriver(Sqlsrv $driver) + { + $this->driver = $driver; + return $this; + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return Connection + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Set connection parameters + * + * @param array $connectionParameters + * @return Connection + */ + public function setConnectionParameters(array $connectionParameters) + { + $this->connectionParameters = $connectionParameters; + return $this; + } + + /** + * Get connection parameters + * + * @return array + */ + public function getConnectionParameters() + { + return $this->connectionParameters; + } + + /** + * Get current schema + * + * @return string + */ + public function getCurrentSchema() + { + if (!$this->isConnected()) { + $this->connect(); + } + + $result = sqlsrv_query($this->resource, 'SELECT SCHEMA_NAME()'); + $r = sqlsrv_fetch_array($result); + return $r[0]; + } + + /** + * Set resource + * + * @param resource $resource + * @throws Exception\InvalidArgumentException + * @return Connection + */ + public function setResource($resource) + { + if (get_resource_type($resource) !== 'SQL Server Connection') { + throw new Exception\InvalidArgumentException('Resource provided was not of type SQL Server Connection'); + } + $this->resource = $resource; + return $this; + } + + /** + * @return resource + */ + public function getResource() + { + if (!$this->isConnected()) { + $this->connect(); + } + return $this->resource; + } + + /** + * Connect + * + * @throws Exception\RuntimeException + * @return Connection + */ + public function connect() + { + if ($this->resource) { + return $this; + } + + $serverName = '.'; + $params = array( + 'ReturnDatesAsStrings' => true + ); + foreach ($this->connectionParameters as $key => $value) { + switch (strtolower($key)) { + case 'hostname': + case 'servername': + $serverName = (string) $value; + break; + case 'username': + case 'uid': + $params['UID'] = (string) $value; + break; + case 'password': + case 'pwd': + $params['PWD'] = (string) $value; + break; + case 'database': + case 'dbname': + $params['Database'] = (string) $value; + break; + case 'charset': + $params['CharacterSet'] = (string) $value; + break; + case 'driver_options': + case 'options': + $params = array_merge($params, (array) $value); + break; + + } + } + + $this->resource = sqlsrv_connect($serverName, $params); + + if (!$this->resource) { + throw new Exception\RuntimeException( + 'Connect Error', + null, + new ErrorException(sqlsrv_errors()) + ); + } + + return $this; + } + + /** + * Is connected + * @return bool + */ + public function isConnected() + { + return (is_resource($this->resource)); + } + + /** + * Disconnect + */ + public function disconnect() + { + sqlsrv_close($this->resource); + $this->resource = null; + } + + /** + * Begin transaction + */ + public function beginTransaction() + { + if (!$this->resource) { + $this->connect(); + } + if (sqlsrv_begin_transaction($this->resource) === false) { + throw new Exception\RuntimeException( + 'Begin transaction failed', + null, + new ErrorException(sqlsrv_errors()) + ); + } + + $this->inTransaction = true; + } + + /** + * In transaction + * + * @return bool + */ + public function inTransaction() + { + return $this->inTransaction; + } + + /** + * Commit + */ + public function commit() + { + // http://msdn.microsoft.com/en-us/library/cc296194.aspx + + if (!$this->resource) { + $this->connect(); + } + + $this->inTransaction = false; + + return sqlsrv_commit($this->resource); + } + + /** + * Rollback + */ + public function rollback() + { + // http://msdn.microsoft.com/en-us/library/cc296176.aspx + + if (!$this->resource) { + throw new Exception\RuntimeException('Must be connected before you can rollback.'); + } + + return sqlsrv_rollback($this->resource); + } + + /** + * Execute + * + * @param string $sql + * @throws Exception\RuntimeException + * @return mixed + */ + public function execute($sql) + { + if (!$this->isConnected()) { + $this->connect(); + } + + if (!$this->driver instanceof Sqlsrv) { + throw new Exception\RuntimeException('Connection is missing an instance of Sqlsrv'); + } + + if ($this->profiler) { + $this->profiler->profilerStart($sql); + } + + $returnValue = sqlsrv_query($this->resource, $sql); + + if ($this->profiler) { + $this->profiler->profilerFinish($sql); + } + + // if the returnValue is something other than a Sqlsrv_result, bypass wrapping it + if ($returnValue === false) { + $errors = sqlsrv_errors(); + // ignore general warnings + if ($errors[0]['SQLSTATE'] != '01000') { + throw new Exception\RuntimeException( + 'An exception occurred while trying to execute the provided $sql', + null, + new ErrorException($errors) + ); + } + } + + $result = $this->driver->createResult($returnValue); + return $result; + } + + /** + * Prepare + * + * @param string $sql + * @return string + */ + public function prepare($sql) + { + if (!$this->isConnected()) { + $this->connect(); + } + + $statement = $this->driver->createStatement($sql); + return $statement; + } + + /** + * Get last generated id + * + * @param string $name + * @return mixed + */ + public function getLastGeneratedValue($name = null) + { + if (!$this->resource) { + $this->connect(); + } + $sql = 'SELECT @@IDENTITY as Current_Identity'; + $result = sqlsrv_query($this->resource, $sql); + $row = sqlsrv_fetch_array($result); + return $row['Current_Identity']; + } +} diff --git a/library/Zend/Db/Adapter/Driver/Sqlsrv/Exception/ErrorException.php b/library/Zend/Db/Adapter/Driver/Sqlsrv/Exception/ErrorException.php new file mode 100755 index 0000000000..9976eee637 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Sqlsrv/Exception/ErrorException.php @@ -0,0 +1,32 @@ +errors = ($errors === false) ? sqlsrv_errors() : $errors; + } +} diff --git a/library/Zend/Db/Adapter/Driver/Sqlsrv/Exception/ExceptionInterface.php b/library/Zend/Db/Adapter/Driver/Sqlsrv/Exception/ExceptionInterface.php new file mode 100755 index 0000000000..a7168e8d6e --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Sqlsrv/Exception/ExceptionInterface.php @@ -0,0 +1,16 @@ +resource = $resource; + $this->generatedValue = $generatedValue; + return $this; + } + + /** + * @return null + */ + public function buffer() + { + return null; + } + + /** + * @return bool + */ + public function isBuffered() + { + return false; + } + + /** + * Get resource + * + * @return resource + */ + public function getResource() + { + return $this->resource; + } + + /** + * Current + * + * @return mixed + */ + public function current() + { + if ($this->currentComplete) { + return $this->currentData; + } + + $this->load(); + return $this->currentData; + } + + /** + * Next + * + * @return bool + */ + public function next() + { + $this->load(); + return true; + } + + /** + * Load + * + * @param int $row + * @return mixed + */ + protected function load($row = SQLSRV_SCROLL_NEXT) + { + $this->currentData = sqlsrv_fetch_array($this->resource, SQLSRV_FETCH_ASSOC, $row); + $this->currentComplete = true; + $this->position++; + return $this->currentData; + } + + /** + * Key + * + * @return mixed + */ + public function key() + { + return $this->position; + } + + /** + * Rewind + * + * @return bool + */ + public function rewind() + { + $this->position = 0; + $this->load(SQLSRV_SCROLL_FIRST); + return true; + } + + /** + * Valid + * + * @return bool + */ + public function valid() + { + if ($this->currentComplete && $this->currentData) { + return true; + } + + return $this->load(); + } + + /** + * Count + * + * @return int + */ + public function count() + { + return sqlsrv_num_rows($this->resource); + } + + /** + * @return bool|int + */ + public function getFieldCount() + { + return sqlsrv_num_fields($this->resource); + } + + /** + * Is query result + * + * @return bool + */ + public function isQueryResult() + { + if (is_bool($this->resource)) { + return false; + } + return (sqlsrv_num_fields($this->resource) > 0); + } + + /** + * Get affected rows + * + * @return int + */ + public function getAffectedRows() + { + return sqlsrv_rows_affected($this->resource); + } + + /** + * @return mixed|null + */ + public function getGeneratedValue() + { + return $this->generatedValue; + } +} diff --git a/library/Zend/Db/Adapter/Driver/Sqlsrv/Sqlsrv.php b/library/Zend/Db/Adapter/Driver/Sqlsrv/Sqlsrv.php new file mode 100755 index 0000000000..0cb8b24356 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Sqlsrv/Sqlsrv.php @@ -0,0 +1,211 @@ +registerConnection($connection); + $this->registerStatementPrototype(($statementPrototype) ?: new Statement()); + $this->registerResultPrototype(($resultPrototype) ?: new Result()); + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return Sqlsrv + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + if ($this->connection instanceof Profiler\ProfilerAwareInterface) { + $this->connection->setProfiler($profiler); + } + if ($this->statementPrototype instanceof Profiler\ProfilerAwareInterface) { + $this->statementPrototype->setProfiler($profiler); + } + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * Register connection + * + * @param Connection $connection + * @return Sqlsrv + */ + public function registerConnection(Connection $connection) + { + $this->connection = $connection; + $this->connection->setDriver($this); + return $this; + } + + /** + * Register statement prototype + * + * @param Statement $statementPrototype + * @return Sqlsrv + */ + public function registerStatementPrototype(Statement $statementPrototype) + { + $this->statementPrototype = $statementPrototype; + $this->statementPrototype->setDriver($this); + return $this; + } + + /** + * Register result prototype + * + * @param Result $resultPrototype + * @return Sqlsrv + */ + public function registerResultPrototype(Result $resultPrototype) + { + $this->resultPrototype = $resultPrototype; + return $this; + } + + /** + * Get database paltform name + * + * @param string $nameFormat + * @return string + */ + public function getDatabasePlatformName($nameFormat = self::NAME_FORMAT_CAMELCASE) + { + if ($nameFormat == self::NAME_FORMAT_CAMELCASE) { + return 'SqlServer'; + } + + return 'SQLServer'; + } + + /** + * Check environment + * + * @throws Exception\RuntimeException + * @return void + */ + public function checkEnvironment() + { + if (!extension_loaded('sqlsrv')) { + throw new Exception\RuntimeException('The Sqlsrv extension is required for this adapter but the extension is not loaded'); + } + } + + /** + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * @param string|resource $sqlOrResource + * @return Statement + */ + public function createStatement($sqlOrResource = null) + { + $statement = clone $this->statementPrototype; + if (is_resource($sqlOrResource)) { + $statement->initialize($sqlOrResource); + } else { + if (!$this->connection->isConnected()) { + $this->connection->connect(); + } + $statement->initialize($this->connection->getResource()); + if (is_string($sqlOrResource)) { + $statement->setSql($sqlOrResource); + } elseif ($sqlOrResource != null) { + throw new Exception\InvalidArgumentException('createStatement() only accepts an SQL string or a Sqlsrv resource'); + } + } + return $statement; + } + + /** + * @param resource $resource + * @return Result + */ + public function createResult($resource) + { + $result = clone $this->resultPrototype; + $result->initialize($resource, $this->connection->getLastGeneratedValue()); + return $result; + } + + /** + * @return array + */ + public function getPrepareType() + { + return self::PARAMETERIZATION_POSITIONAL; + } + + /** + * @param string $name + * @param mixed $type + * @return string + */ + public function formatParameterName($name, $type = null) + { + return '?'; + } + + /** + * @return mixed + */ + public function getLastGeneratedValue() + { + return $this->getConnection()->getLastGeneratedValue(); + } +} diff --git a/library/Zend/Db/Adapter/Driver/Sqlsrv/Statement.php b/library/Zend/Db/Adapter/Driver/Sqlsrv/Statement.php new file mode 100755 index 0000000000..4aefa9ff90 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/Sqlsrv/Statement.php @@ -0,0 +1,313 @@ +driver = $driver; + return $this; + } + + /** + * @param Profiler\ProfilerInterface $profiler + * @return Statement + */ + public function setProfiler(Profiler\ProfilerInterface $profiler) + { + $this->profiler = $profiler; + return $this; + } + + /** + * @return null|Profiler\ProfilerInterface + */ + public function getProfiler() + { + return $this->profiler; + } + + /** + * + * One of two resource types will be provided here: + * a) "SQL Server Connection" when a prepared statement needs to still be produced + * b) "SQL Server Statement" when a prepared statement has been already produced + * (there will need to already be a bound param set if it applies to this query) + * + * @param resource $resource + * @throws Exception\InvalidArgumentException + * @return Statement + */ + public function initialize($resource) + { + $resourceType = get_resource_type($resource); + + if ($resourceType == 'SQL Server Connection') { + $this->sqlsrv = $resource; + } elseif ($resourceType == 'SQL Server Statement') { + $this->resource = $resource; + $this->isPrepared = true; + } else { + throw new Exception\InvalidArgumentException('Invalid resource provided to ' . __CLASS__); + } + + return $this; + } + + /** + * Set parameter container + * + * @param ParameterContainer $parameterContainer + * @return Statement + */ + public function setParameterContainer(ParameterContainer $parameterContainer) + { + $this->parameterContainer = $parameterContainer; + return $this; + } + + /** + * @return ParameterContainer + */ + public function getParameterContainer() + { + return $this->parameterContainer; + } + + /** + * @param $resource + * @return Statement + */ + public function setResource($resource) + { + $this->resource = $resource; + return $this; + } + + /** + * Get resource + * + * @return resource + */ + public function getResource() + { + return $this->resource; + } + + /** + * @param string $sql + * @return Statement + */ + public function setSql($sql) + { + $this->sql = $sql; + return $this; + } + + /** + * Get sql + * + * @return string + */ + public function getSql() + { + return $this->sql; + } + + /** + * @param string $sql + * @param array $options + * @throws Exception\RuntimeException + * @return Statement + */ + public function prepare($sql = null, array $options = array()) + { + if ($this->isPrepared) { + throw new Exception\RuntimeException('Already prepared'); + } + $sql = ($sql) ?: $this->sql; + $options = ($options) ?: $this->prepareOptions; + + $pRef = &$this->parameterReferences; + for ($position = 0, $count = substr_count($sql, '?'); $position < $count; $position++) { + if (!isset($this->prepareParams[$position])) { + $pRef[$position] = array('', SQLSRV_PARAM_IN, null, null); + } else { + $pRef[$position] = &$this->prepareParams[$position]; + } + } + + $this->resource = sqlsrv_prepare($this->sqlsrv, $sql, $pRef, $options); + + $this->isPrepared = true; + + return $this; + } + + /** + * @return bool + */ + public function isPrepared() + { + return $this->isPrepared; + } + + /** + * Execute + * + * @param array|ParameterContainer $parameters + * @throws Exception\RuntimeException + * @return Result + */ + public function execute($parameters = null) + { + /** END Standard ParameterContainer Merging Block */ + if (!$this->isPrepared) { + $this->prepare(); + } + + /** START Standard ParameterContainer Merging Block */ + if (!$this->parameterContainer instanceof ParameterContainer) { + if ($parameters instanceof ParameterContainer) { + $this->parameterContainer = $parameters; + $parameters = null; + } else { + $this->parameterContainer = new ParameterContainer(); + } + } + + if (is_array($parameters)) { + $this->parameterContainer->setFromArray($parameters); + } + + if ($this->parameterContainer->count() > 0) { + $this->bindParametersFromContainer(); + } + + if ($this->profiler) { + $this->profiler->profilerStart($this); + } + + $resultValue = sqlsrv_execute($this->resource); + + if ($this->profiler) { + $this->profiler->profilerFinish(); + } + + if ($resultValue === false) { + $errors = sqlsrv_errors(); + // ignore general warnings + if ($errors[0]['SQLSTATE'] != '01000') { + throw new Exception\RuntimeException($errors[0]['message']); + } + } + + $result = $this->driver->createResult($this->resource); + return $result; + } + + /** + * Bind parameters from container + * + */ + protected function bindParametersFromContainer() + { + $values = $this->parameterContainer->getPositionalArray(); + $position = 0; + foreach ($values as $value) { + $this->parameterReferences[$position++][0] = $value; + } + } + + /** + * @param array $prepareParams + */ + public function setPrepareParams(array $prepareParams) + { + $this->prepareParams = $prepareParams; + } + + /** + * @param array $prepareOptions + */ + public function setPrepareOptions(array $prepareOptions) + { + $this->prepareOptions = $prepareOptions; + } +} diff --git a/library/Zend/Db/Adapter/Driver/StatementInterface.php b/library/Zend/Db/Adapter/Driver/StatementInterface.php new file mode 100755 index 0000000000..a1ba567095 --- /dev/null +++ b/library/Zend/Db/Adapter/Driver/StatementInterface.php @@ -0,0 +1,44 @@ +parameters = $parameters; + } +} diff --git a/library/Zend/Db/Adapter/Exception/InvalidQueryException.php b/library/Zend/Db/Adapter/Exception/InvalidQueryException.php new file mode 100755 index 0000000000..1372237fe1 --- /dev/null +++ b/library/Zend/Db/Adapter/Exception/InvalidQueryException.php @@ -0,0 +1,14 @@ +setFromArray($data); + } + } + + /** + * Offset exists + * + * @param string $name + * @return bool + */ + public function offsetExists($name) + { + return (isset($this->data[$name])); + } + + /** + * Offset get + * + * @param string $name + * @return mixed + */ + public function offsetGet($name) + { + return (isset($this->data[$name])) ? $this->data[$name] : null; + } + + /** + * @param $name + * @param $from + */ + public function offsetSetReference($name, $from) + { + $this->data[$name] =& $this->data[$from]; + } + + /** + * Offset set + * + * @param string|int $name + * @param mixed $value + * @param mixed $errata + */ + public function offsetSet($name, $value, $errata = null) + { + $position = false; + + // if integer, get name for this position + if (is_int($name)) { + if (isset($this->positions[$name])) { + $position = $name; + $name = $this->positions[$name]; + } else { + $name = (string) $name; + } + } elseif (is_string($name)) { + // is a string: + $position = array_key_exists($name, $this->data); + } elseif ($name === null) { + $name = (string) count($this->data); + } else { + throw new Exception\InvalidArgumentException('Keys must be string, integer or null'); + } + + if ($position === false) { + $this->positions[] = $name; + } + + $this->data[$name] = $value; + + if ($errata) { + $this->offsetSetErrata($name, $errata); + } + } + + /** + * Offset unset + * + * @param string $name + * @return ParameterContainer + */ + public function offsetUnset($name) + { + if (is_int($name) && isset($this->positions[$name])) { + $name = $this->positions[$name]; + } + unset($this->data[$name]); + return $this; + } + + /** + * Set from array + * + * @param array $data + * @return ParameterContainer + */ + public function setFromArray(Array $data) + { + foreach ($data as $n => $v) { + $this->offsetSet($n, $v); + } + return $this; + } + + /** + * Offset set errata + * + * @param string|int $name + * @param mixed $errata + */ + public function offsetSetErrata($name, $errata) + { + if (is_int($name)) { + $name = $this->positions[$name]; + } + $this->errata[$name] = $errata; + } + + /** + * Offset get errata + * + * @param string|int $name + * @throws Exception\InvalidArgumentException + * @return mixed + */ + public function offsetGetErrata($name) + { + if (is_int($name)) { + $name = $this->positions[$name]; + } + if (!array_key_exists($name, $this->data)) { + throw new Exception\InvalidArgumentException('Data does not exist for this name/position'); + } + return $this->errata[$name]; + } + + /** + * Offset has errata + * + * @param string|int $name + * @return bool + */ + public function offsetHasErrata($name) + { + if (is_int($name)) { + $name = $this->positions[$name]; + } + return (isset($this->errata[$name])); + } + + /** + * Offset unset errata + * + * @param string|int $name + * @throws Exception\InvalidArgumentException + */ + public function offsetUnsetErrata($name) + { + if (is_int($name)) { + $name = $this->positions[$name]; + } + if (!array_key_exists($name, $this->errata)) { + throw new Exception\InvalidArgumentException('Data does not exist for this name/position'); + } + $this->errata[$name] = null; + } + + /** + * Get errata iterator + * + * @return \ArrayIterator + */ + public function getErrataIterator() + { + return new \ArrayIterator($this->errata); + } + + /** + * getNamedArray + * + * @return array + */ + public function getNamedArray() + { + return $this->data; + } + + /** + * getNamedArray + * + * @return array + */ + public function getPositionalArray() + { + return array_values($this->data); + } + + /** + * count + * + * @return int + */ + public function count() + { + return count($this->data); + } + + /** + * Current + * + * @return mixed + */ + public function current() + { + return current($this->data); + } + + /** + * Next + * + * @return mixed + */ + public function next() + { + return next($this->data); + } + + /** + * Key + * + * @return mixed + */ + public function key() + { + return key($this->data); + } + + /** + * Valid + * + * @return bool + */ + public function valid() + { + return (current($this->data) !== false); + } + + /** + * Rewind + */ + public function rewind() + { + reset($this->data); + } + + /** + * @param array|ParameterContainer $parameters + * @throws Exception\InvalidArgumentException + * @return ParameterContainer + */ + public function merge($parameters) + { + if (!is_array($parameters) && !$parameters instanceof ParameterContainer) { + throw new Exception\InvalidArgumentException('$parameters must be an array or an instance of ParameterContainer'); + } + + if (count($parameters) == 0) { + return $this; + } + + if ($parameters instanceof ParameterContainer) { + $parameters = $parameters->getNamedArray(); + } + + foreach ($parameters as $key => $value) { + if (is_int($key)) { + $key = null; + } + $this->offsetSet($key, $value); + } + return $this; + } +} diff --git a/library/Zend/Db/Adapter/Platform/IbmDb2.php b/library/Zend/Db/Adapter/Platform/IbmDb2.php new file mode 100755 index 0000000000..693b647e83 --- /dev/null +++ b/library/Zend/Db/Adapter/Platform/IbmDb2.php @@ -0,0 +1,207 @@ +quoteIdentifiers = false; + } + + if (isset($options['identifier_separator'])) { + $this->identifierSeparator = $options['identifier_separator']; + } + } + + /** + * Get name + * + * @return string + */ + public function getName() + { + return 'IBM DB2'; + } + + /** + * Get quote indentifier symbol + * + * @return string + */ + public function getQuoteIdentifierSymbol() + { + return '"'; + } + + /** + * Quote identifier + * + * @param string $identifier + * @return string + */ + public function quoteIdentifier($identifier) + { + if ($this->quoteIdentifiers === false) { + return $identifier; + } + return '"' . str_replace('"', '\\' . '"', $identifier) . '"'; + } + + /** + * Quote identifier chain + * + * @param string|string[] $identifierChain + * @return string + */ + public function quoteIdentifierChain($identifierChain) + { + if ($this->quoteIdentifiers === false) { + return (is_array($identifierChain)) ? implode($this->identifierSeparator, $identifierChain) : $identifierChain; + } + $identifierChain = str_replace('"', '\\"', $identifierChain); + if (is_array($identifierChain)) { + $identifierChain = implode('"' . $this->identifierSeparator . '"', $identifierChain); + } + return '"' . $identifierChain . '"'; + } + + /** + * Get quote value symbol + * + * @return string + */ + public function getQuoteValueSymbol() + { + return '\''; + } + + /** + * Quote value + * + * @param string $value + * @return string + */ + public function quoteValue($value) + { + if (function_exists('db2_escape_string')) { + return '\'' . db2_escape_string($value) . '\''; + } + trigger_error( + 'Attempting to quote a value in ' . __CLASS__ . ' without extension/driver support ' + . 'can introduce security vulnerabilities in a production environment.' + ); + return '\'' . str_replace("'", "''", $value) . '\''; + } + + /** + * Quote Trusted Value + * + * The ability to quote values without notices + * + * @param $value + * @return mixed + */ + public function quoteTrustedValue($value) + { + if (function_exists('db2_escape_string')) { + return '\'' . db2_escape_string($value) . '\''; + } + return '\'' . str_replace("'", "''", $value) . '\''; + } + + /** + * Quote value list + * + * @param string|string[] $valueList + * @return string + */ + public function quoteValueList($valueList) + { + if (!is_array($valueList)) { + return $this->quoteValue($valueList); + } + + $value = reset($valueList); + do { + $valueList[key($valueList)] = $this->quoteValue($value); + } while ($value = next($valueList)); + return implode(', ', $valueList); + } + + /** + * Get identifier separator + * + * @return string + */ + public function getIdentifierSeparator() + { + return $this->identifierSeparator; + } + + /** + * Quote identifier in fragment + * + * @param string $identifier + * @param array $safeWords + * @return string + */ + public function quoteIdentifierInFragment($identifier, array $safeWords = array()) + { + if ($this->quoteIdentifiers === false) { + return $identifier; + } + $parts = preg_split('#([\.\s\W])#', $identifier, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + + if ($safeWords) { + $safeWords = array_flip($safeWords); + $safeWords = array_change_key_case($safeWords, CASE_LOWER); + } + foreach ($parts as $i => $part) { + if ($safeWords && isset($safeWords[strtolower($part)])) { + continue; + } + + switch ($part) { + case ' ': + case '.': + case '*': + case 'AS': + case 'As': + case 'aS': + case 'as': + break; + default: + $parts[$i] = '"' . str_replace('"', '\\' . '"', $part) . '"'; + } + } + + return implode('', $parts); + } +} diff --git a/library/Zend/Db/Adapter/Platform/Mysql.php b/library/Zend/Db/Adapter/Platform/Mysql.php new file mode 100755 index 0000000000..6e02f083ab --- /dev/null +++ b/library/Zend/Db/Adapter/Platform/Mysql.php @@ -0,0 +1,214 @@ +setDriver($driver); + } + } + + /** + * @param \Zend\Db\Adapter\Driver\Mysqli\Mysqli|\Zend\Db\Adapter\Driver\Pdo\Pdo||\mysqli|\PDO $driver + * @throws \Zend\Db\Adapter\Exception\InvalidArgumentException + * @return $this + */ + public function setDriver($driver) + { + // handle Zend\Db drivers + if ($driver instanceof Mysqli\Mysqli + || ($driver instanceof Pdo\Pdo && $driver->getDatabasePlatformName() == 'Mysql') + || ($driver instanceof \mysqli) + || ($driver instanceof \PDO && $driver->getAttribute(\PDO::ATTR_DRIVER_NAME) == 'mysql') + ) { + $this->resource = $driver; + return $this; + } + + throw new Exception\InvalidArgumentException('$driver must be a Mysqli or Mysql PDO Zend\Db\Adapter\Driver, Mysqli instance or MySQL PDO instance'); + } + + /** + * Get name + * + * @return string + */ + public function getName() + { + return 'MySQL'; + } + + /** + * Get quote identifier symbol + * + * @return string + */ + public function getQuoteIdentifierSymbol() + { + return '`'; + } + + /** + * Quote identifier + * + * @param string $identifier + * @return string + */ + public function quoteIdentifier($identifier) + { + return '`' . str_replace('`', '``', $identifier) . '`'; + } + + /** + * Quote identifier chain + * + * @param string|string[] $identifierChain + * @return string + */ + public function quoteIdentifierChain($identifierChain) + { + $identifierChain = str_replace('`', '``', $identifierChain); + if (is_array($identifierChain)) { + $identifierChain = implode('`.`', $identifierChain); + } + return '`' . $identifierChain . '`'; + } + + /** + * Get quote value symbol + * + * @return string + */ + public function getQuoteValueSymbol() + { + return '\''; + } + + /** + * Quote value + * + * @param string $value + * @return string + */ + public function quoteValue($value) + { + if ($this->resource instanceof DriverInterface) { + $this->resource = $this->resource->getConnection()->getResource(); + } + if ($this->resource instanceof \mysqli) { + return '\'' . $this->resource->real_escape_string($value) . '\''; + } + if ($this->resource instanceof \PDO) { + return $this->resource->quote($value); + } + trigger_error( + 'Attempting to quote a value in ' . __CLASS__ . ' without extension/driver support ' + . 'can introduce security vulnerabilities in a production environment.' + ); + return '\'' . addcslashes($value, "\x00\n\r\\'\"\x1a") . '\''; + } + + /** + * Quote Trusted Value + * + * The ability to quote values without notices + * + * @param $value + * @return mixed + */ + public function quoteTrustedValue($value) + { + if ($this->resource instanceof DriverInterface) { + $this->resource = $this->resource->getConnection()->getResource(); + } + if ($this->resource instanceof \mysqli) { + return '\'' . $this->resource->real_escape_string($value) . '\''; + } + if ($this->resource instanceof \PDO) { + return $this->resource->quote($value); + } + return '\'' . addcslashes($value, "\x00\n\r\\'\"\x1a") . '\''; + } + + /** + * Quote value list + * + * @param string|string[] $valueList + * @return string + */ + public function quoteValueList($valueList) + { + if (!is_array($valueList)) { + return $this->quoteValue($valueList); + } + + $value = reset($valueList); + do { + $valueList[key($valueList)] = $this->quoteValue($value); + } while ($value = next($valueList)); + return implode(', ', $valueList); + } + + /** + * Get identifier separator + * + * @return string + */ + public function getIdentifierSeparator() + { + return '.'; + } + + /** + * Quote identifier in fragment + * + * @param string $identifier + * @param array $safeWords + * @return string + */ + public function quoteIdentifierInFragment($identifier, array $safeWords = array()) + { + // regex taken from @link http://dev.mysql.com/doc/refman/5.0/en/identifiers.html + $parts = preg_split('#([^0-9,a-z,A-Z$_])#', $identifier, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + if ($safeWords) { + $safeWords = array_flip($safeWords); + $safeWords = array_change_key_case($safeWords, CASE_LOWER); + } + foreach ($parts as $i => $part) { + if ($safeWords && isset($safeWords[strtolower($part)])) { + continue; + } + switch ($part) { + case ' ': + case '.': + case '*': + case 'AS': + case 'As': + case 'aS': + case 'as': + break; + default: + $parts[$i] = '`' . str_replace('`', '``', $part) . '`'; + } + } + return implode('', $parts); + } +} diff --git a/library/Zend/Db/Adapter/Platform/Oracle.php b/library/Zend/Db/Adapter/Platform/Oracle.php new file mode 100755 index 0000000000..61f700a49a --- /dev/null +++ b/library/Zend/Db/Adapter/Platform/Oracle.php @@ -0,0 +1,187 @@ +quoteIdentifiers = false; + } + } + + /** + * Get name + * + * @return string + */ + public function getName() + { + return 'Oracle'; + } + + /** + * Get quote identifier symbol + * + * @return string + */ + public function getQuoteIdentifierSymbol() + { + return '"'; + } + + /** + * Quote identifier + * + * @param string $identifier + * @return string + */ + public function quoteIdentifier($identifier) + { + if ($this->quoteIdentifiers === false) { + return $identifier; + } + return '"' . str_replace('"', '\\' . '"', $identifier) . '"'; + } + + /** + * Quote identifier chain + * + * @param string|string[] $identifierChain + * @return string + */ + public function quoteIdentifierChain($identifierChain) + { + if ($this->quoteIdentifiers === false) { + return (is_array($identifierChain)) ? implode('.', $identifierChain) : $identifierChain; + } + $identifierChain = str_replace('"', '\\"', $identifierChain); + if (is_array($identifierChain)) { + $identifierChain = implode('"."', $identifierChain); + } + return '"' . $identifierChain . '"'; + } + + /** + * Get quote value symbol + * + * @return string + */ + public function getQuoteValueSymbol() + { + return '\''; + } + + /** + * Quote value + * + * @param string $value + * @return string + */ + public function quoteValue($value) + { + trigger_error( + 'Attempting to quote a value in ' . __CLASS__ . ' without extension/driver support ' + . 'can introduce security vulnerabilities in a production environment.' + ); + return '\'' . addcslashes($value, "\x00\n\r\\'\"\x1a") . '\''; + } + + /** + * Quote Trusted Value + * + * The ability to quote values without notices + * + * @param $value + * @return mixed + */ + public function quoteTrustedValue($value) + { + return '\'' . addcslashes($value, "\x00\n\r\\'\"\x1a") . '\''; + } + + /** + * Quote value list + * + * @param string|string[] $valueList + * @return string + */ + public function quoteValueList($valueList) + { + if (!is_array($valueList)) { + return $this->quoteValue($valueList); + } + + $value = reset($valueList); + do { + $valueList[key($valueList)] = $this->quoteValue($value); + } while ($value = next($valueList)); + return implode(', ', $valueList); + } + + /** + * Get identifier separator + * + * @return string + */ + public function getIdentifierSeparator() + { + return '.'; + } + + /** + * Quote identifier in fragment + * + * @param string $identifier + * @param array $safeWords + * @return string + */ + public function quoteIdentifierInFragment($identifier, array $safeWords = array()) + { + if ($this->quoteIdentifiers === false) { + return $identifier; + } + $parts = preg_split('#([\.\s\W])#', $identifier, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + if ($safeWords) { + $safeWords = array_flip($safeWords); + $safeWords = array_change_key_case($safeWords, CASE_LOWER); + } + foreach ($parts as $i => $part) { + if ($safeWords && isset($safeWords[strtolower($part)])) { + continue; + } + switch ($part) { + case ' ': + case '.': + case '*': + case 'AS': + case 'As': + case 'aS': + case 'as': + break; + default: + $parts[$i] = '"' . str_replace('"', '\\' . '"', $part) . '"'; + } + } + return implode('', $parts); + } +} diff --git a/library/Zend/Db/Adapter/Platform/PlatformInterface.php b/library/Zend/Db/Adapter/Platform/PlatformInterface.php new file mode 100755 index 0000000000..d8ec05b2be --- /dev/null +++ b/library/Zend/Db/Adapter/Platform/PlatformInterface.php @@ -0,0 +1,94 @@ +setDriver($driver); + } + } + + /** + * @param \Zend\Db\Adapter\Driver\Pgsql\Pgsql|\Zend\Db\Adapter\Driver\Pdo\Pdo|resource|\PDO $driver + * @throws \Zend\Db\Adapter\Exception\InvalidArgumentException + * @return $this + */ + public function setDriver($driver) + { + if ($driver instanceof Pgsql\Pgsql + || ($driver instanceof Pdo\Pdo && $driver->getDatabasePlatformName() == 'Postgresql') + || (is_resource($driver) && (in_array(get_resource_type($driver), array('pgsql link', 'pgsql link persistent')))) + || ($driver instanceof \PDO && $driver->getAttribute(\PDO::ATTR_DRIVER_NAME) == 'pgsql') + ) { + $this->resource = $driver; + return $this; + } + + throw new Exception\InvalidArgumentException('$driver must be a Pgsql or Postgresql PDO Zend\Db\Adapter\Driver, pgsql link resource or Postgresql PDO instance'); + } + + /** + * Get name + * + * @return string + */ + public function getName() + { + return 'PostgreSQL'; + } + + /** + * Get quote indentifier symbol + * + * @return string + */ + public function getQuoteIdentifierSymbol() + { + return '"'; + } + + /** + * Quote identifier + * + * @param string $identifier + * @return string + */ + public function quoteIdentifier($identifier) + { + return '"' . str_replace('"', '\\' . '"', $identifier) . '"'; + } + + /** + * Quote identifier chain + * + * @param string|string[] $identifierChain + * @return string + */ + public function quoteIdentifierChain($identifierChain) + { + $identifierChain = str_replace('"', '\\"', $identifierChain); + if (is_array($identifierChain)) { + $identifierChain = implode('"."', $identifierChain); + } + return '"' . $identifierChain . '"'; + } + + /** + * Get quote value symbol + * + * @return string + */ + public function getQuoteValueSymbol() + { + return '\''; + } + + /** + * Quote value + * + * @param string $value + * @return string + */ + public function quoteValue($value) + { + if ($this->resource instanceof DriverInterface) { + $this->resource = $this->resource->getConnection()->getResource(); + } + if (is_resource($this->resource)) { + return '\'' . pg_escape_string($this->resource, $value) . '\''; + } + if ($this->resource instanceof \PDO) { + return $this->resource->quote($value); + } + trigger_error( + 'Attempting to quote a value in ' . __CLASS__ . ' without extension/driver support ' + . 'can introduce security vulnerabilities in a production environment.' + ); + return '\'' . addcslashes($value, "\x00\n\r\\'\"\x1a") . '\''; + } + + /** + * Quote Trusted Value + * + * The ability to quote values without notices + * + * @param $value + * @return mixed + */ + public function quoteTrustedValue($value) + { + if ($this->resource instanceof DriverInterface) { + $this->resource = $this->resource->getConnection()->getResource(); + } + if (is_resource($this->resource)) { + return '\'' . pg_escape_string($this->resource, $value) . '\''; + } + if ($this->resource instanceof \PDO) { + return $this->resource->quote($value); + } + return '\'' . addcslashes($value, "\x00\n\r\\'\"\x1a") . '\''; + } + + /** + * Quote value list + * + * @param string|string[] $valueList + * @return string + */ + public function quoteValueList($valueList) + { + if (!is_array($valueList)) { + return $this->quoteValue($valueList); + } + + $value = reset($valueList); + do { + $valueList[key($valueList)] = $this->quoteValue($value); + } while ($value = next($valueList)); + return implode(', ', $valueList); + } + + /** + * Get identifier separator + * + * @return string + */ + public function getIdentifierSeparator() + { + return '.'; + } + + /** + * Quote identifier in fragment + * + * @param string $identifier + * @param array $safeWords + * @return string + */ + public function quoteIdentifierInFragment($identifier, array $safeWords = array()) + { + $parts = preg_split('#([\.\s\W])#', $identifier, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + if ($safeWords) { + $safeWords = array_flip($safeWords); + $safeWords = array_change_key_case($safeWords, CASE_LOWER); + } + foreach ($parts as $i => $part) { + if ($safeWords && isset($safeWords[strtolower($part)])) { + continue; + } + switch ($part) { + case ' ': + case '.': + case '*': + case 'AS': + case 'As': + case 'aS': + case 'as': + break; + default: + $parts[$i] = '"' . str_replace('"', '\\' . '"', $part) . '"'; + } + } + return implode('', $parts); + } +} diff --git a/library/Zend/Db/Adapter/Platform/Sql92.php b/library/Zend/Db/Adapter/Platform/Sql92.php new file mode 100755 index 0000000000..bbeda46d7e --- /dev/null +++ b/library/Zend/Db/Adapter/Platform/Sql92.php @@ -0,0 +1,161 @@ +quoteValue($valueList); + } + + $value = reset($valueList); + do { + $valueList[key($valueList)] = $this->quoteValue($value); + } while ($value = next($valueList)); + return implode(', ', $valueList); + } + + /** + * Get identifier separator + * + * @return string + */ + public function getIdentifierSeparator() + { + return '.'; + } + + /** + * Quote identifier in fragment + * + * @param string $identifier + * @param array $safeWords + * @return string + */ + public function quoteIdentifierInFragment($identifier, array $safeWords = array()) + { + $parts = preg_split('#([\.\s\W])#', $identifier, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + if ($safeWords) { + $safeWords = array_flip($safeWords); + $safeWords = array_change_key_case($safeWords, CASE_LOWER); + } + foreach ($parts as $i => $part) { + if ($safeWords && isset($safeWords[strtolower($part)])) { + continue; + } + + switch ($part) { + case ' ': + case '.': + case '*': + case 'AS': + case 'As': + case 'aS': + case 'as': + break; + default: + $parts[$i] = '"' . str_replace('"', '\\' . '"', $part) . '"'; + } + } + + return implode('', $parts); + } +} diff --git a/library/Zend/Db/Adapter/Platform/SqlServer.php b/library/Zend/Db/Adapter/Platform/SqlServer.php new file mode 100755 index 0000000000..74a9acb9cc --- /dev/null +++ b/library/Zend/Db/Adapter/Platform/SqlServer.php @@ -0,0 +1,203 @@ +setDriver($driver); + } + } + + /** + * @param \Zend\Db\Adapter\Driver\Sqlsrv\Sqlsrv|\Zend\Db\Adapter\Driver\Pdo\Pdo||resource|\PDO $driver + * @throws \Zend\Db\Adapter\Exception\InvalidArgumentException + * @return $this + */ + public function setDriver($driver) + { + // handle Zend\Db drivers + if (($driver instanceof Pdo\Pdo && in_array($driver->getDatabasePlatformName(), array('SqlServer', 'Dblib'))) + || (($driver instanceof \PDO && in_array($driver->getAttribute(\PDO::ATTR_DRIVER_NAME), array('sqlsrv', 'dblib')))) + ) { + $this->resource = $driver; + return $this; + } + + throw new Exception\InvalidArgumentException('$driver must be a Sqlsrv PDO Zend\Db\Adapter\Driver or Sqlsrv PDO instance'); + } + + /** + * Get name + * + * @return string + */ + public function getName() + { + return 'SQLServer'; + } + + /** + * Get quote identifier symbol + * + * @return string + */ + public function getQuoteIdentifierSymbol() + { + return array('[', ']'); + } + + /** + * Quote identifier + * + * @param string $identifier + * @return string + */ + public function quoteIdentifier($identifier) + { + return '[' . $identifier . ']'; + } + + /** + * Quote identifier chain + * + * @param string|string[] $identifierChain + * @return string + */ + public function quoteIdentifierChain($identifierChain) + { + if (is_array($identifierChain)) { + $identifierChain = implode('].[', $identifierChain); + } + return '[' . $identifierChain . ']'; + } + + /** + * Get quote value symbol + * + * @return string + */ + public function getQuoteValueSymbol() + { + return '\''; + } + + /** + * Quote value + * + * @param string $value + * @return string + */ + public function quoteValue($value) + { + if ($this->resource instanceof DriverInterface) { + $this->resource = $this->resource->getConnection()->getResource(); + } + if ($this->resource instanceof \PDO) { + return $this->resource->quote($value); + } + trigger_error( + 'Attempting to quote a value in ' . __CLASS__ . ' without extension/driver support ' + . 'can introduce security vulnerabilities in a production environment.' + ); + $value = addcslashes($value, "\000\032"); + return '\'' . str_replace('\'', '\'\'', $value) . '\''; + } + + /** + * Quote Trusted Value + * + * The ability to quote values without notices + * + * @param $value + * @return mixed + */ + public function quoteTrustedValue($value) + { + if ($this->resource instanceof DriverInterface) { + $this->resource = $this->resource->getConnection()->getResource(); + } + if ($this->resource instanceof \PDO) { + return $this->resource->quote($value); + } + return '\'' . str_replace('\'', '\'\'', $value) . '\''; + } + + /** + * Quote value list + * + * @param string|string[] $valueList + * @return string + */ + public function quoteValueList($valueList) + { + if (!is_array($valueList)) { + return $this->quoteValue($valueList); + } + $value = reset($valueList); + do { + $valueList[key($valueList)] = $this->quoteValue($value); + } while ($value = next($valueList)); + return implode(', ', $valueList); + } + + /** + * Get identifier separator + * + * @return string + */ + public function getIdentifierSeparator() + { + return '.'; + } + + /** + * Quote identifier in fragment + * + * @param string $identifier + * @param array $safeWords + * @return string + */ + public function quoteIdentifierInFragment($identifier, array $safeWords = array()) + { + $parts = preg_split('#([\.\s\W])#', $identifier, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + if ($safeWords) { + $safeWords = array_flip($safeWords); + $safeWords = array_change_key_case($safeWords, CASE_LOWER); + } + foreach ($parts as $i => $part) { + if ($safeWords && isset($safeWords[strtolower($part)])) { + continue; + } + switch ($part) { + case ' ': + case '.': + case '*': + case 'AS': + case 'As': + case 'aS': + case 'as': + break; + default: + $parts[$i] = '[' . $part . ']'; + } + } + return implode('', $parts); + } +} diff --git a/library/Zend/Db/Adapter/Platform/Sqlite.php b/library/Zend/Db/Adapter/Platform/Sqlite.php new file mode 100755 index 0000000000..340c247e34 --- /dev/null +++ b/library/Zend/Db/Adapter/Platform/Sqlite.php @@ -0,0 +1,210 @@ +setDriver($driver); + } + } + + /** + * @param \Zend\Db\Adapter\Driver\Pdo\Pdo||\PDO $driver + * @throws \Zend\Db\Adapter\Exception\InvalidArgumentException + * @return $this + */ + public function setDriver($driver) + { + if (($driver instanceof \PDO && $driver->getAttribute(\PDO::ATTR_DRIVER_NAME) == 'sqlite') + || ($driver instanceof Pdo\Pdo && $driver->getDatabasePlatformName() == 'Sqlite') + ) { + $this->resource = $driver; + return $this; + } + + throw new Exception\InvalidArgumentException('$driver must be a Sqlite PDO Zend\Db\Adapter\Driver, Sqlite PDO instance'); + } + + /** + * Get name + * + * @return string + */ + public function getName() + { + return 'SQLite'; + } + + /** + * Get quote identifier symbol + * + * @return string + */ + public function getQuoteIdentifierSymbol() + { + return '"'; + } + + /** + * Quote identifier + * + * @param string $identifier + * @return string + */ + public function quoteIdentifier($identifier) + { + return '"' . str_replace('"', '\\' . '"', $identifier) . '"'; + } + + /** + * Quote identifier chain + * + * @param string|string[] $identifierChain + * @return string + */ + public function quoteIdentifierChain($identifierChain) + { + $identifierChain = str_replace('"', '\\"', $identifierChain); + if (is_array($identifierChain)) { + $identifierChain = implode('"."', $identifierChain); + } + return '"' . $identifierChain . '"'; + } + + /** + * Get quote value symbol + * + * @return string + */ + public function getQuoteValueSymbol() + { + return '\''; + } + + /** + * Quote value + * + * @param string $value + * @return string + */ + public function quoteValue($value) + { + $resource = $this->resource; + + if ($resource instanceof DriverInterface) { + $resource = $resource->getConnection()->getResource(); + } + + if ($resource instanceof \PDO) { + return $resource->quote($value); + } + + trigger_error( + 'Attempting to quote a value in ' . __CLASS__ . ' without extension/driver support ' + . 'can introduce security vulnerabilities in a production environment.' + ); + return '\'' . addcslashes($value, "\x00\n\r\\'\"\x1a") . '\''; + } + + /** + * Quote Trusted Value + * + * The ability to quote values without notices + * + * @param $value + * @return mixed + */ + public function quoteTrustedValue($value) + { + $resource = $this->resource; + + if ($resource instanceof DriverInterface) { + $resource = $resource->getConnection()->getResource(); + } + + if ($resource instanceof \PDO) { + return $resource->quote($value); + } + + return '\'' . addcslashes($value, "\x00\n\r\\'\"\x1a") . '\''; + } + + /** + * Quote value list + * + * @param string|string[] $valueList + * @return string + */ + public function quoteValueList($valueList) + { + if (!is_array($valueList)) { + return $this->quoteValue($valueList); + } + $value = reset($valueList); + do { + $valueList[key($valueList)] = $this->quoteValue($value); + } while ($value = next($valueList)); + return implode(', ', $valueList); + } + + /** + * Get identifier separator + * + * @return string + */ + public function getIdentifierSeparator() + { + return '.'; + } + + /** + * Quote identifier in fragment + * + * @param string $identifier + * @param array $safeWords + * @return string + */ + public function quoteIdentifierInFragment($identifier, array $safeWords = array()) + { + $parts = preg_split('#([\.\s\W])#', $identifier, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + if ($safeWords) { + $safeWords = array_flip($safeWords); + $safeWords = array_change_key_case($safeWords, CASE_LOWER); + } + foreach ($parts as $i => $part) { + if ($safeWords && isset($safeWords[strtolower($part)])) { + continue; + } + switch ($part) { + case ' ': + case '.': + case '*': + case 'AS': + case 'As': + case 'aS': + case 'as': + break; + default: + $parts[$i] = '"' . str_replace('"', '\\' . '"', $part) . '"'; + } + } + return implode('', $parts); + } +} diff --git a/library/Zend/Db/Adapter/Profiler/Profiler.php b/library/Zend/Db/Adapter/Profiler/Profiler.php new file mode 100755 index 0000000000..5115e3f3f1 --- /dev/null +++ b/library/Zend/Db/Adapter/Profiler/Profiler.php @@ -0,0 +1,85 @@ + '', + 'parameters' => null, + 'start' => microtime(true), + 'end' => null, + 'elapse' => null + ); + if ($target instanceof StatementContainerInterface) { + $profileInformation['sql'] = $target->getSql(); + $profileInformation['parameters'] = clone $target->getParameterContainer(); + } elseif (is_string($target)) { + $profileInformation['sql'] = $target; + } else { + throw new Exception\InvalidArgumentException(__FUNCTION__ . ' takes either a StatementContainer or a string'); + } + + $this->profiles[$this->currentIndex] = $profileInformation; + + return $this; + } + + /** + * @return Profiler + */ + public function profilerFinish() + { + if (!isset($this->profiles[$this->currentIndex])) { + throw new Exception\RuntimeException('A profile must be started before ' . __FUNCTION__ . ' can be called.'); + } + $current = &$this->profiles[$this->currentIndex]; + $current['end'] = microtime(true); + $current['elapse'] = $current['end'] - $current['start']; + $this->currentIndex++; + return $this; + } + + /** + * @return array|null + */ + public function getLastProfile() + { + return end($this->profiles); + } + + /** + * @return array + */ + public function getProfiles() + { + return $this->profiles; + } +} diff --git a/library/Zend/Db/Adapter/Profiler/ProfilerAwareInterface.php b/library/Zend/Db/Adapter/Profiler/ProfilerAwareInterface.php new file mode 100755 index 0000000000..a0b631d94b --- /dev/null +++ b/library/Zend/Db/Adapter/Profiler/ProfilerAwareInterface.php @@ -0,0 +1,15 @@ +setSql($sql); + } + $this->parameterContainer = ($parameterContainer) ?: new ParameterContainer; + } + + /** + * @param $sql + * @return StatementContainer + */ + public function setSql($sql) + { + $this->sql = $sql; + return $this; + } + + /** + * @return string + */ + public function getSql() + { + return $this->sql; + } + + /** + * @param ParameterContainer $parameterContainer + * @return StatementContainer + */ + public function setParameterContainer(ParameterContainer $parameterContainer) + { + $this->parameterContainer = $parameterContainer; + return $this; + } + + /** + * @return null|ParameterContainer + */ + public function getParameterContainer() + { + return $this->parameterContainer; + } +} diff --git a/library/Zend/Db/Adapter/StatementContainerInterface.php b/library/Zend/Db/Adapter/StatementContainerInterface.php new file mode 100755 index 0000000000..098d6a6fde --- /dev/null +++ b/library/Zend/Db/Adapter/StatementContainerInterface.php @@ -0,0 +1,43 @@ +adapter = $adapter; + $this->source = $this->createSourceFromAdapter($adapter); + } + + /** + * Create source from adapter + * + * @param Adapter $adapter + * @return Source\AbstractSource + */ + protected function createSourceFromAdapter(Adapter $adapter) + { + switch ($adapter->getPlatform()->getName()) { + case 'MySQL': + return new Source\MysqlMetadata($adapter); + case 'SQLServer': + return new Source\SqlServerMetadata($adapter); + case 'SQLite': + return new Source\SqliteMetadata($adapter); + case 'PostgreSQL': + return new Source\PostgresqlMetadata($adapter); + case 'Oracle': + return new Source\OracleMetadata($adapter); + } + + throw new \Exception('cannot create source from adapter'); + } + + // @todo methods + + /** + * Get base tables and views + * + * @param string $schema + * @param bool $includeViews + * @return Object\TableObject[] + */ + public function getTables($schema = null, $includeViews = false) + { + return $this->source->getTables($schema, $includeViews); + } + + /** + * Get base tables and views + * + * @param string $schema + * @return Object\TableObject[] + */ + public function getViews($schema = null) + { + return $this->source->getViews($schema); + } + + /** + * Get triggers + * + * @param string $schema + * @return array + */ + public function getTriggers($schema = null) + { + return $this->source->getTriggers($schema); + } + + /** + * Get constraints + * + * @param string $table + * @param string $schema + * @return array + */ + public function getConstraints($table, $schema = null) + { + return $this->source->getConstraints($table, $schema); + } + + /** + * Get columns + * + * @param string $table + * @param string $schema + * @return array + */ + public function getColumns($table, $schema = null) + { + return $this->source->getColumns($table, $schema); + } + + /** + * Get constraint keys + * + * @param string $constraint + * @param string $table + * @param string $schema + * @return array + */ + public function getConstraintKeys($constraint, $table, $schema = null) + { + return $this->source->getConstraintKeys($constraint, $table, $schema); + } + + /** + * Get constraints + * + * @param string $constraintName + * @param string $table + * @param string $schema + * @return Object\ConstraintObject + */ + public function getConstraint($constraintName, $table, $schema = null) + { + return $this->source->getConstraint($constraintName, $table, $schema); + } + + /** + * Get schemas + */ + public function getSchemas() + { + return $this->source->getSchemas(); + } + + /** + * Get table names + * + * @param string $schema + * @param bool $includeViews + * @return array + */ + public function getTableNames($schema = null, $includeViews = false) + { + return $this->source->getTableNames($schema, $includeViews); + } + + /** + * Get table + * + * @param string $tableName + * @param string $schema + * @return Object\TableObject + */ + public function getTable($tableName, $schema = null) + { + return $this->source->getTable($tableName, $schema); + } + + /** + * Get views names + * + * @param string $schema + * @return \Zend\Db\Metadata\Object\TableObject + */ + public function getViewNames($schema = null) + { + return $this->source->getViewNames($schema); + } + + /** + * Get view + * + * @param string $viewName + * @param string $schema + * @return \Zend\Db\Metadata\Object\TableObject + */ + public function getView($viewName, $schema = null) + { + return $this->source->getView($viewName, $schema); + } + + /** + * Get trigger names + * + * @param string $schema + * @return array + */ + public function getTriggerNames($schema = null) + { + return $this->source->getTriggerNames($schema); + } + + /** + * Get trigger + * + * @param string $triggerName + * @param string $schema + * @return \Zend\Db\Metadata\Object\TriggerObject + */ + public function getTrigger($triggerName, $schema = null) + { + return $this->source->getTrigger($triggerName, $schema); + } + + /** + * Get column names + * + * @param string $table + * @param string $schema + * @return array + */ + public function getColumnNames($table, $schema = null) + { + return $this->source->getColumnNames($table, $schema); + } + + /** + * Get column + * + * @param string $columnName + * @param string $table + * @param string $schema + * @return \Zend\Db\Metadata\Object\ColumnObject + */ + public function getColumn($columnName, $table, $schema = null) + { + return $this->source->getColumn($columnName, $table, $schema); + } +} diff --git a/library/Zend/Db/Metadata/MetadataInterface.php b/library/Zend/Db/Metadata/MetadataInterface.php new file mode 100755 index 0000000000..f0f58eb30b --- /dev/null +++ b/library/Zend/Db/Metadata/MetadataInterface.php @@ -0,0 +1,35 @@ +setName($name); + } + } + + /** + * Set columns + * + * @param array $columns + */ + public function setColumns(array $columns) + { + $this->columns = $columns; + } + + /** + * Get columns + * + * @return array + */ + public function getColumns() + { + return $this->columns; + } + + /** + * Set constraints + * + * @param array $constraints + */ + public function setConstraints($constraints) + { + $this->constraints = $constraints; + } + + /** + * Get constraints + * + * @return array + */ + public function getConstraints() + { + return $this->constraints; + } + + /** + * Set name + * + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Get name + * + * @return string + */ + public function getName() + { + return $this->name; + } +} diff --git a/library/Zend/Db/Metadata/Object/ColumnObject.php b/library/Zend/Db/Metadata/Object/ColumnObject.php new file mode 100755 index 0000000000..e76a91a996 --- /dev/null +++ b/library/Zend/Db/Metadata/Object/ColumnObject.php @@ -0,0 +1,388 @@ +setName($name); + $this->setTableName($tableName); + $this->setSchemaName($schemaName); + } + + /** + * Set name + * + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Get name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Get table name + * + * @return string + */ + public function getTableName() + { + return $this->tableName; + } + + /** + * Set table name + * + * @param string $tableName + * @return ColumnObject + */ + public function setTableName($tableName) + { + $this->tableName = $tableName; + return $this; + } + + /** + * Set schema name + * + * @param string $schemaName + */ + public function setSchemaName($schemaName) + { + $this->schemaName = $schemaName; + } + + /** + * Get schema name + * + * @return string + */ + public function getSchemaName() + { + return $this->schemaName; + } + + /** + * @return int $ordinalPosition + */ + public function getOrdinalPosition() + { + return $this->ordinalPosition; + } + + /** + * @param int $ordinalPosition to set + * @return ColumnObject + */ + public function setOrdinalPosition($ordinalPosition) + { + $this->ordinalPosition = $ordinalPosition; + return $this; + } + + /** + * @return null|string the $columnDefault + */ + public function getColumnDefault() + { + return $this->columnDefault; + } + + /** + * @param mixed $columnDefault to set + * @return ColumnObject + */ + public function setColumnDefault($columnDefault) + { + $this->columnDefault = $columnDefault; + return $this; + } + + /** + * @return bool $isNullable + */ + public function getIsNullable() + { + return $this->isNullable; + } + + /** + * @param bool $isNullable to set + * @return ColumnObject + */ + public function setIsNullable($isNullable) + { + $this->isNullable = $isNullable; + return $this; + } + + /** + * @return bool $isNullable + */ + public function isNullable() + { + return $this->isNullable; + } + + /** + * @return null|string the $dataType + */ + public function getDataType() + { + return $this->dataType; + } + + /** + * @param string $dataType the $dataType to set + * @return ColumnObject + */ + public function setDataType($dataType) + { + $this->dataType = $dataType; + return $this; + } + + /** + * @return int|null the $characterMaximumLength + */ + public function getCharacterMaximumLength() + { + return $this->characterMaximumLength; + } + + /** + * @param int $characterMaximumLength the $characterMaximumLength to set + * @return ColumnObject + */ + public function setCharacterMaximumLength($characterMaximumLength) + { + $this->characterMaximumLength = $characterMaximumLength; + return $this; + } + + /** + * @return int|null the $characterOctetLength + */ + public function getCharacterOctetLength() + { + return $this->characterOctetLength; + } + + /** + * @param int $characterOctetLength the $characterOctetLength to set + * @return ColumnObject + */ + public function setCharacterOctetLength($characterOctetLength) + { + $this->characterOctetLength = $characterOctetLength; + return $this; + } + + /** + * @return int the $numericPrecision + */ + public function getNumericPrecision() + { + return $this->numericPrecision; + } + + /** + * @param int $numericPrecision the $numericPrevision to set + * @return ColumnObject + */ + public function setNumericPrecision($numericPrecision) + { + $this->numericPrecision = $numericPrecision; + return $this; + } + + /** + * @return int the $numericScale + */ + public function getNumericScale() + { + return $this->numericScale; + } + + /** + * @param int $numericScale the $numericScale to set + * @return ColumnObject + */ + public function setNumericScale($numericScale) + { + $this->numericScale = $numericScale; + return $this; + } + + /** + * @return bool + */ + public function getNumericUnsigned() + { + return $this->numericUnsigned; + } + + /** + * @param bool $numericUnsigned + * @return ColumnObject + */ + public function setNumericUnsigned($numericUnsigned) + { + $this->numericUnsigned = $numericUnsigned; + return $this; + } + + /** + * @return bool + */ + public function isNumericUnsigned() + { + return $this->numericUnsigned; + } + + /** + * @return array the $errata + */ + public function getErratas() + { + return $this->errata; + } + + /** + * @param array $erratas + * @return ColumnObject + */ + public function setErratas(array $erratas) + { + foreach ($erratas as $name => $value) { + $this->setErrata($name, $value); + } + return $this; + } + + /** + * @param string $errataName + * @return mixed + */ + public function getErrata($errataName) + { + if (array_key_exists($errataName, $this->errata)) { + return $this->errata[$errataName]; + } + return null; + } + + /** + * @param string $errataName + * @param mixed $errataValue + * @return ColumnObject + */ + public function setErrata($errataName, $errataValue) + { + $this->errata[$errataName] = $errataValue; + return $this; + } +} diff --git a/library/Zend/Db/Metadata/Object/ConstraintKeyObject.php b/library/Zend/Db/Metadata/Object/ConstraintKeyObject.php new file mode 100755 index 0000000000..5683688057 --- /dev/null +++ b/library/Zend/Db/Metadata/Object/ConstraintKeyObject.php @@ -0,0 +1,249 @@ +setColumnName($column); + } + + /** + * Get column name + * + * @return string + */ + public function getColumnName() + { + return $this->columnName; + } + + /** + * Set column name + * + * @param string $columnName + * @return ConstraintKeyObject + */ + public function setColumnName($columnName) + { + $this->columnName = $columnName; + return $this; + } + + /** + * Get ordinal position + * + * @return int + */ + public function getOrdinalPosition() + { + return $this->ordinalPosition; + } + + /** + * Set ordinal position + * + * @param int $ordinalPosition + * @return ConstraintKeyObject + */ + public function setOrdinalPosition($ordinalPosition) + { + $this->ordinalPosition = $ordinalPosition; + return $this; + } + + /** + * Get position in unique constraint + * + * @return bool + */ + public function getPositionInUniqueConstraint() + { + return $this->positionInUniqueConstraint; + } + + /** + * Set position in unique constraint + * + * @param bool $positionInUniqueConstraint + * @return ConstraintKeyObject + */ + public function setPositionInUniqueConstraint($positionInUniqueConstraint) + { + $this->positionInUniqueConstraint = $positionInUniqueConstraint; + return $this; + } + + /** + * Get referencred table schema + * + * @return string + */ + public function getReferencedTableSchema() + { + return $this->referencedTableSchema; + } + + /** + * Set referenced table schema + * + * @param string $referencedTableSchema + * @return ConstraintKeyObject + */ + public function setReferencedTableSchema($referencedTableSchema) + { + $this->referencedTableSchema = $referencedTableSchema; + return $this; + } + + /** + * Get referenced table name + * + * @return string + */ + public function getReferencedTableName() + { + return $this->referencedTableName; + } + + /** + * Set Referenced table name + * + * @param string $referencedTableName + * @return ConstraintKeyObject + */ + public function setReferencedTableName($referencedTableName) + { + $this->referencedTableName = $referencedTableName; + return $this; + } + + /** + * Get referenced column name + * + * @return string + */ + public function getReferencedColumnName() + { + return $this->referencedColumnName; + } + + /** + * Set referenced column name + * + * @param string $referencedColumnName + * @return ConstraintKeyObject + */ + public function setReferencedColumnName($referencedColumnName) + { + $this->referencedColumnName = $referencedColumnName; + return $this; + } + + /** + * set foreign key update rule + * + * @param string $foreignKeyUpdateRule + */ + public function setForeignKeyUpdateRule($foreignKeyUpdateRule) + { + $this->foreignKeyUpdateRule = $foreignKeyUpdateRule; + } + + /** + * Get foreign key update rule + * + * @return string + */ + public function getForeignKeyUpdateRule() + { + return $this->foreignKeyUpdateRule; + } + + /** + * Set foreign key delete rule + * + * @param string $foreignKeyDeleteRule + */ + public function setForeignKeyDeleteRule($foreignKeyDeleteRule) + { + $this->foreignKeyDeleteRule = $foreignKeyDeleteRule; + } + + /** + * get foreign key delete rule + * + * @return string + */ + public function getForeignKeyDeleteRule() + { + return $this->foreignKeyDeleteRule; + } +} diff --git a/library/Zend/Db/Metadata/Object/ConstraintObject.php b/library/Zend/Db/Metadata/Object/ConstraintObject.php new file mode 100755 index 0000000000..089c5ea1fb --- /dev/null +++ b/library/Zend/Db/Metadata/Object/ConstraintObject.php @@ -0,0 +1,411 @@ +setName($name); + $this->setTableName($tableName); + $this->setSchemaName($schemaName); + } + + /** + * Set name + * + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Get name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set schema name + * + * @param string $schemaName + */ + public function setSchemaName($schemaName) + { + $this->schemaName = $schemaName; + } + + /** + * Get schema name + * + * @return string + */ + public function getSchemaName() + { + return $this->schemaName; + } + + /** + * Get table name + * + * @return string + */ + public function getTableName() + { + return $this->tableName; + } + + /** + * Set table name + * + * @param string $tableName + * @return ConstraintObject + */ + public function setTableName($tableName) + { + $this->tableName = $tableName; + return $this; + } + + /** + * Set type + * + * @param string $type + */ + public function setType($type) + { + $this->type = $type; + } + + /** + * Get type + * + * @return string + */ + public function getType() + { + return $this->type; + } + + public function hasColumns() + { + return (!empty($this->columns)); + } + + /** + * Get Columns. + * + * @return string[] + */ + public function getColumns() + { + return $this->columns; + } + + /** + * Set Columns. + * + * @param string[] $columns + * @return ConstraintObject + */ + public function setColumns(array $columns) + { + $this->columns = $columns; + return $this; + } + + /** + * Get Referenced Table Schema. + * + * @return string + */ + public function getReferencedTableSchema() + { + return $this->referencedTableSchema; + } + + /** + * Set Referenced Table Schema. + * + * @param string $referencedTableSchema + * @return ConstraintObject + */ + public function setReferencedTableSchema($referencedTableSchema) + { + $this->referencedTableSchema = $referencedTableSchema; + return $this; + } + + /** + * Get Referenced Table Name. + * + * @return string + */ + public function getReferencedTableName() + { + return $this->referencedTableName; + } + + /** + * Set Referenced Table Name. + * + * @param string $referencedTableName + * @return ConstraintObject + */ + public function setReferencedTableName($referencedTableName) + { + $this->referencedTableName = $referencedTableName; + return $this; + } + + /** + * Get Referenced Columns. + * + * @return string[] + */ + public function getReferencedColumns() + { + return $this->referencedColumns; + } + + /** + * Set Referenced Columns. + * + * @param string[] $referencedColumns + * @return ConstraintObject + */ + public function setReferencedColumns(array $referencedColumns) + { + $this->referencedColumns = $referencedColumns; + return $this; + } + + /** + * Get Match Option. + * + * @return string + */ + public function getMatchOption() + { + return $this->matchOption; + } + + /** + * Set Match Option. + * + * @param string $matchOption + * @return ConstraintObject + */ + public function setMatchOption($matchOption) + { + $this->matchOption = $matchOption; + return $this; + } + + /** + * Get Update Rule. + * + * @return string + */ + public function getUpdateRule() + { + return $this->updateRule; + } + + /** + * Set Update Rule. + * + * @param string $updateRule + * @return ConstraintObject + */ + public function setUpdateRule($updateRule) + { + $this->updateRule = $updateRule; + return $this; + } + + /** + * Get Delete Rule. + * + * @return string + */ + public function getDeleteRule() + { + return $this->deleteRule; + } + + /** + * Set Delete Rule. + * + * @param string $deleteRule + * @return ConstraintObject + */ + public function setDeleteRule($deleteRule) + { + $this->deleteRule = $deleteRule; + return $this; + } + + /** + * Get Check Clause. + * + * @return string + */ + public function getCheckClause() + { + return $this->checkClause; + } + + /** + * Set Check Clause. + * + * @param string $checkClause + * @return ConstraintObject + */ + public function setCheckClause($checkClause) + { + $this->checkClause = $checkClause; + return $this; + } + + /** + * Is primary key + * + * @return bool + */ + public function isPrimaryKey() + { + return ('PRIMARY KEY' == $this->type); + } + + /** + * Is unique key + * + * @return bool + */ + public function isUnique() + { + return ('UNIQUE' == $this->type); + } + + /** + * Is foreign key + * + * @return bool + */ + public function isForeignKey() + { + return ('FOREIGN KEY' == $this->type); + } + + /** + * Is foreign key + * + * @return bool + */ + public function isCheck() + { + return ('CHECK' == $this->type); + } +} diff --git a/library/Zend/Db/Metadata/Object/TableObject.php b/library/Zend/Db/Metadata/Object/TableObject.php new file mode 100755 index 0000000000..8735fbfc8c --- /dev/null +++ b/library/Zend/Db/Metadata/Object/TableObject.php @@ -0,0 +1,14 @@ +name; + } + + /** + * Set Name. + * + * @param string $name + * @return TriggerObject + */ + public function setName($name) + { + $this->name = $name; + return $this; + } + + /** + * Get Event Manipulation. + * + * @return string + */ + public function getEventManipulation() + { + return $this->eventManipulation; + } + + /** + * Set Event Manipulation. + * + * @param string $eventManipulation + * @return TriggerObject + */ + public function setEventManipulation($eventManipulation) + { + $this->eventManipulation = $eventManipulation; + return $this; + } + + /** + * Get Event Object Catalog. + * + * @return string + */ + public function getEventObjectCatalog() + { + return $this->eventObjectCatalog; + } + + /** + * Set Event Object Catalog. + * + * @param string $eventObjectCatalog + * @return TriggerObject + */ + public function setEventObjectCatalog($eventObjectCatalog) + { + $this->eventObjectCatalog = $eventObjectCatalog; + return $this; + } + + /** + * Get Event Object Schema. + * + * @return string + */ + public function getEventObjectSchema() + { + return $this->eventObjectSchema; + } + + /** + * Set Event Object Schema. + * + * @param string $eventObjectSchema + * @return TriggerObject + */ + public function setEventObjectSchema($eventObjectSchema) + { + $this->eventObjectSchema = $eventObjectSchema; + return $this; + } + + /** + * Get Event Object Table. + * + * @return string + */ + public function getEventObjectTable() + { + return $this->eventObjectTable; + } + + /** + * Set Event Object Table. + * + * @param string $eventObjectTable + * @return TriggerObject + */ + public function setEventObjectTable($eventObjectTable) + { + $this->eventObjectTable = $eventObjectTable; + return $this; + } + + /** + * Get Action Order. + * + * @return string + */ + public function getActionOrder() + { + return $this->actionOrder; + } + + /** + * Set Action Order. + * + * @param string $actionOrder + * @return TriggerObject + */ + public function setActionOrder($actionOrder) + { + $this->actionOrder = $actionOrder; + return $this; + } + + /** + * Get Action Condition. + * + * @return string + */ + public function getActionCondition() + { + return $this->actionCondition; + } + + /** + * Set Action Condition. + * + * @param string $actionCondition + * @return TriggerObject + */ + public function setActionCondition($actionCondition) + { + $this->actionCondition = $actionCondition; + return $this; + } + + /** + * Get Action Statement. + * + * @return string + */ + public function getActionStatement() + { + return $this->actionStatement; + } + + /** + * Set Action Statement. + * + * @param string $actionStatement + * @return TriggerObject + */ + public function setActionStatement($actionStatement) + { + $this->actionStatement = $actionStatement; + return $this; + } + + /** + * Get Action Orientation. + * + * @return string + */ + public function getActionOrientation() + { + return $this->actionOrientation; + } + + /** + * Set Action Orientation. + * + * @param string $actionOrientation + * @return TriggerObject + */ + public function setActionOrientation($actionOrientation) + { + $this->actionOrientation = $actionOrientation; + return $this; + } + + /** + * Get Action Timing. + * + * @return string + */ + public function getActionTiming() + { + return $this->actionTiming; + } + + /** + * Set Action Timing. + * + * @param string $actionTiming + * @return TriggerObject + */ + public function setActionTiming($actionTiming) + { + $this->actionTiming = $actionTiming; + return $this; + } + + /** + * Get Action Reference Old Table. + * + * @return string + */ + public function getActionReferenceOldTable() + { + return $this->actionReferenceOldTable; + } + + /** + * Set Action Reference Old Table. + * + * @param string $actionReferenceOldTable + * @return TriggerObject + */ + public function setActionReferenceOldTable($actionReferenceOldTable) + { + $this->actionReferenceOldTable = $actionReferenceOldTable; + return $this; + } + + /** + * Get Action Reference New Table. + * + * @return string + */ + public function getActionReferenceNewTable() + { + return $this->actionReferenceNewTable; + } + + /** + * Set Action Reference New Table. + * + * @param string $actionReferenceNewTable + * @return TriggerObject + */ + public function setActionReferenceNewTable($actionReferenceNewTable) + { + $this->actionReferenceNewTable = $actionReferenceNewTable; + return $this; + } + + /** + * Get Action Reference Old Row. + * + * @return string + */ + public function getActionReferenceOldRow() + { + return $this->actionReferenceOldRow; + } + + /** + * Set Action Reference Old Row. + * + * @param string $actionReferenceOldRow + * @return TriggerObject + */ + public function setActionReferenceOldRow($actionReferenceOldRow) + { + $this->actionReferenceOldRow = $actionReferenceOldRow; + return $this; + } + + /** + * Get Action Reference New Row. + * + * @return string + */ + public function getActionReferenceNewRow() + { + return $this->actionReferenceNewRow; + } + + /** + * Set Action Reference New Row. + * + * @param string $actionReferenceNewRow + * @return TriggerObject + */ + public function setActionReferenceNewRow($actionReferenceNewRow) + { + $this->actionReferenceNewRow = $actionReferenceNewRow; + return $this; + } + + /** + * Get Created. + * + * @return \DateTime + */ + public function getCreated() + { + return $this->created; + } + + /** + * Set Created. + * + * @param \DateTime $created + * @return TriggerObject + */ + public function setCreated($created) + { + $this->created = $created; + return $this; + } +} diff --git a/library/Zend/Db/Metadata/Object/ViewObject.php b/library/Zend/Db/Metadata/Object/ViewObject.php new file mode 100755 index 0000000000..5130e9ecc6 --- /dev/null +++ b/library/Zend/Db/Metadata/Object/ViewObject.php @@ -0,0 +1,76 @@ +viewDefinition; + } + + /** + * @param string $viewDefinition to set + * @return ViewObject + */ + public function setViewDefinition($viewDefinition) + { + $this->viewDefinition = $viewDefinition; + return $this; + } + + /** + * @return string $checkOption + */ + public function getCheckOption() + { + return $this->checkOption; + } + + /** + * @param string $checkOption to set + * @return ViewObject + */ + public function setCheckOption($checkOption) + { + $this->checkOption = $checkOption; + return $this; + } + + /** + * @return bool $isUpdatable + */ + public function getIsUpdatable() + { + return $this->isUpdatable; + } + + /** + * @param bool $isUpdatable to set + * @return ViewObject + */ + public function setIsUpdatable($isUpdatable) + { + $this->isUpdatable = $isUpdatable; + return $this; + } + + public function isUpdatable() + { + return $this->isUpdatable; + } +} diff --git a/library/Zend/Db/Metadata/Source/AbstractSource.php b/library/Zend/Db/Metadata/Source/AbstractSource.php new file mode 100755 index 0000000000..63d63a928e --- /dev/null +++ b/library/Zend/Db/Metadata/Source/AbstractSource.php @@ -0,0 +1,601 @@ +adapter = $adapter; + $this->defaultSchema = ($adapter->getCurrentSchema()) ?: self::DEFAULT_SCHEMA; + } + + /** + * Get schemas + * + */ + public function getSchemas() + { + $this->loadSchemaData(); + + return $this->data['schemas']; + } + + /** + * Get table names + * + * @param string $schema + * @param bool $includeViews + * @return string[] + */ + public function getTableNames($schema = null, $includeViews = false) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadTableNameData($schema); + + if ($includeViews) { + return array_keys($this->data['table_names'][$schema]); + } + + $tableNames = array(); + foreach ($this->data['table_names'][$schema] as $tableName => $data) { + if ('BASE TABLE' == $data['table_type']) { + $tableNames[] = $tableName; + } + } + return $tableNames; + } + + /** + * Get tables + * + * @param string $schema + * @param bool $includeViews + * @return Object\TableObject[] + */ + public function getTables($schema = null, $includeViews = false) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $tables = array(); + foreach ($this->getTableNames($schema, $includeViews) as $tableName) { + $tables[] = $this->getTable($tableName, $schema); + } + return $tables; + } + + /** + * Get table + * + * @param string $tableName + * @param string $schema + * @return Object\TableObject + */ + public function getTable($tableName, $schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadTableNameData($schema); + + if (!isset($this->data['table_names'][$schema][$tableName])) { + throw new \Exception('Table "' . $tableName . '" does not exist'); + } + + $data = $this->data['table_names'][$schema][$tableName]; + switch ($data['table_type']) { + case 'BASE TABLE': + $table = new Object\TableObject($tableName); + break; + case 'VIEW': + $table = new Object\ViewObject($tableName); + $table->setViewDefinition($data['view_definition']); + $table->setCheckOption($data['check_option']); + $table->setIsUpdatable($data['is_updatable']); + break; + default: + throw new \Exception('Table "' . $tableName . '" is of an unsupported type "' . $data['table_type'] . '"'); + } + $table->setColumns($this->getColumns($tableName, $schema)); + $table->setConstraints($this->getConstraints($tableName, $schema)); + return $table; + } + + /** + * Get view names + * + * @param string $schema + * @return array + */ + public function getViewNames($schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadTableNameData($schema); + + $viewNames = array(); + foreach ($this->data['table_names'][$schema] as $tableName => $data) { + if ('VIEW' == $data['table_type']) { + $viewNames[] = $tableName; + } + } + return $viewNames; + } + + /** + * Get views + * + * @param string $schema + * @return array + */ + public function getViews($schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $views = array(); + foreach ($this->getViewNames($schema) as $tableName) { + $views[] = $this->getTable($tableName, $schema); + } + return $views; + } + + /** + * Get view + * + * @param string $viewName + * @param string $schema + * @return \Zend\Db\Metadata\Object\TableObject + */ + public function getView($viewName, $schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadTableNameData($schema); + + $tableNames = $this->data['table_names'][$schema]; + if (isset($tableNames[$viewName]) && 'VIEW' == $tableNames[$viewName]['table_type']) { + return $this->getTable($viewName, $schema); + } + throw new \Exception('View "' . $viewName . '" does not exist'); + } + + /** + * Gt column names + * + * @param string $table + * @param string $schema + * @return array + */ + public function getColumnNames($table, $schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadColumnData($table, $schema); + + if (!isset($this->data['columns'][$schema][$table])) { + throw new \Exception('"' . $table . '" does not exist'); + } + + return array_keys($this->data['columns'][$schema][$table]); + } + + /** + * Get columns + * + * @param string $table + * @param string $schema + * @return array + */ + public function getColumns($table, $schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadColumnData($table, $schema); + + $columns = array(); + foreach ($this->getColumnNames($table, $schema) as $columnName) { + $columns[] = $this->getColumn($columnName, $table, $schema); + } + return $columns; + } + + /** + * Get column + * + * @param string $columnName + * @param string $table + * @param string $schema + * @return Object\ColumnObject + */ + public function getColumn($columnName, $table, $schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadColumnData($table, $schema); + + if (!isset($this->data['columns'][$schema][$table][$columnName])) { + throw new \Exception('A column by that name was not found.'); + } + + $info = $this->data['columns'][$schema][$table][$columnName]; + + $column = new Object\ColumnObject($columnName, $table, $schema); + $props = array( + 'ordinal_position', 'column_default', 'is_nullable', + 'data_type', 'character_maximum_length', 'character_octet_length', + 'numeric_precision', 'numeric_scale', 'numeric_unsigned', + 'erratas' + ); + foreach ($props as $prop) { + if (isset($info[$prop])) { + $column->{'set' . str_replace('_', '', $prop)}($info[$prop]); + } + } + + $column->setOrdinalPosition($info['ordinal_position']); + $column->setColumnDefault($info['column_default']); + $column->setIsNullable($info['is_nullable']); + $column->setDataType($info['data_type']); + $column->setCharacterMaximumLength($info['character_maximum_length']); + $column->setCharacterOctetLength($info['character_octet_length']); + $column->setNumericPrecision($info['numeric_precision']); + $column->setNumericScale($info['numeric_scale']); + $column->setNumericUnsigned($info['numeric_unsigned']); + $column->setErratas($info['erratas']); + + return $column; + } + + /** + * Get constraints + * + * @param string $table + * @param string $schema + * @return array + */ + public function getConstraints($table, $schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadConstraintData($table, $schema); + + $constraints = array(); + foreach (array_keys($this->data['constraints'][$schema][$table]) as $constraintName) { + $constraints[] = $this->getConstraint($constraintName, $table, $schema); + } + + return $constraints; + } + + /** + * Get constraint + * + * @param string $constraintName + * @param string $table + * @param string $schema + * @return Object\ConstraintObject + */ + public function getConstraint($constraintName, $table, $schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadConstraintData($table, $schema); + + if (!isset($this->data['constraints'][$schema][$table][$constraintName])) { + throw new \Exception('Cannot find a constraint by that name in this table'); + } + + $info = $this->data['constraints'][$schema][$table][$constraintName]; + $constraint = new Object\ConstraintObject($constraintName, $table, $schema); + + foreach (array( + 'constraint_type' => 'setType', + 'match_option' => 'setMatchOption', + 'update_rule' => 'setUpdateRule', + 'delete_rule' => 'setDeleteRule', + 'columns' => 'setColumns', + 'referenced_table_schema' => 'setReferencedTableSchema', + 'referenced_table_name' => 'setReferencedTableName', + 'referenced_columns' => 'setReferencedColumns', + 'check_clause' => 'setCheckClause', + ) as $key => $setMethod) { + if (isset($info[$key])) { + $constraint->{$setMethod}($info[$key]); + } + } + + return $constraint; + } + + /** + * Get constraint keys + * + * @param string $constraint + * @param string $table + * @param string $schema + * @return array + */ + public function getConstraintKeys($constraint, $table, $schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadConstraintReferences($table, $schema); + + // organize references first + $references = array(); + foreach ($this->data['constraint_references'][$schema] as $refKeyInfo) { + if ($refKeyInfo['constraint_name'] == $constraint) { + $references[$refKeyInfo['constraint_name']] = $refKeyInfo; + } + } + + $this->loadConstraintDataKeys($schema); + + $keys = array(); + foreach ($this->data['constraint_keys'][$schema] as $constraintKeyInfo) { + if ($constraintKeyInfo['table_name'] == $table && $constraintKeyInfo['constraint_name'] === $constraint) { + $keys[] = $key = new Object\ConstraintKeyObject($constraintKeyInfo['column_name']); + $key->setOrdinalPosition($constraintKeyInfo['ordinal_position']); + if (isset($references[$constraint])) { + //$key->setReferencedTableSchema($constraintKeyInfo['referenced_table_schema']); + $key->setForeignKeyUpdateRule($references[$constraint]['update_rule']); + $key->setForeignKeyDeleteRule($references[$constraint]['delete_rule']); + //$key->setReferencedTableSchema($references[$constraint]['referenced_table_schema']); + $key->setReferencedTableName($references[$constraint]['referenced_table_name']); + $key->setReferencedColumnName($references[$constraint]['referenced_column_name']); + } + } + } + + return $keys; + } + + /** + * Get trigger names + * + * @param string $schema + * @return array + */ + public function getTriggerNames($schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadTriggerData($schema); + + return array_keys($this->data['triggers'][$schema]); + } + + /** + * Get triggers + * + * @param string $schema + * @return array + */ + public function getTriggers($schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $triggers = array(); + foreach ($this->getTriggerNames($schema) as $triggerName) { + $triggers[] = $this->getTrigger($triggerName, $schema); + } + return $triggers; + } + + /** + * Get trigger + * + * @param string $triggerName + * @param string $schema + * @return Object\TriggerObject + */ + public function getTrigger($triggerName, $schema = null) + { + if ($schema === null) { + $schema = $this->defaultSchema; + } + + $this->loadTriggerData($schema); + + if (!isset($this->data['triggers'][$schema][$triggerName])) { + throw new \Exception('Trigger "' . $triggerName . '" does not exist'); + } + + $info = $this->data['triggers'][$schema][$triggerName]; + + $trigger = new Object\TriggerObject(); + + $trigger->setName($triggerName); + $trigger->setEventManipulation($info['event_manipulation']); + $trigger->setEventObjectCatalog($info['event_object_catalog']); + $trigger->setEventObjectSchema($info['event_object_schema']); + $trigger->setEventObjectTable($info['event_object_table']); + $trigger->setActionOrder($info['action_order']); + $trigger->setActionCondition($info['action_condition']); + $trigger->setActionStatement($info['action_statement']); + $trigger->setActionOrientation($info['action_orientation']); + $trigger->setActionTiming($info['action_timing']); + $trigger->setActionReferenceOldTable($info['action_reference_old_table']); + $trigger->setActionReferenceNewTable($info['action_reference_new_table']); + $trigger->setActionReferenceOldRow($info['action_reference_old_row']); + $trigger->setActionReferenceNewRow($info['action_reference_new_row']); + $trigger->setCreated($info['created']); + + return $trigger; + } + + /** + * Prepare data hierarchy + * + * @param string $type + * @param string $key ... + */ + protected function prepareDataHierarchy($type) + { + $data = &$this->data; + foreach (func_get_args() as $key) { + if (!isset($data[$key])) { + $data[$key] = array(); + } + $data = &$data[$key]; + } + } + + /** + * Load schema data + */ + protected function loadSchemaData() + { + } + + /** + * Load table name data + * + * @param string $schema + */ + protected function loadTableNameData($schema) + { + if (isset($this->data['table_names'][$schema])) { + return; + } + + $this->prepareDataHierarchy('table_names', $schema); + } + + /** + * Load column data + * + * @param string $table + * @param string $schema + */ + protected function loadColumnData($table, $schema) + { + if (isset($this->data['columns'][$schema][$table])) { + return; + } + + $this->prepareDataHierarchy('columns', $schema, $table); + } + + /** + * Load constraint data + * + * @param string $table + * @param string $schema + */ + protected function loadConstraintData($table, $schema) + { + if (isset($this->data['constraints'][$schema])) { + return; + } + + $this->prepareDataHierarchy('constraints', $schema); + } + + /** + * Load constraint data keys + * + * @param string $schema + */ + protected function loadConstraintDataKeys($schema) + { + if (isset($this->data['constraint_keys'][$schema])) { + return; + } + + $this->prepareDataHierarchy('constraint_keys', $schema); + } + + /** + * Load constraint references + * + * @param string $table + * @param string $schema + */ + protected function loadConstraintReferences($table, $schema) + { + if (isset($this->data['constraint_references'][$schema])) { + return; + } + + $this->prepareDataHierarchy('constraint_references', $schema); + } + + /** + * Load trigger data + * + * @param string $schema + */ + protected function loadTriggerData($schema) + { + if (isset($this->data['triggers'][$schema])) { + return; + } + + $this->prepareDataHierarchy('triggers', $schema); + } +} diff --git a/library/Zend/Db/Metadata/Source/MysqlMetadata.php b/library/Zend/Db/Metadata/Source/MysqlMetadata.php new file mode 100755 index 0000000000..ac9642f9c1 --- /dev/null +++ b/library/Zend/Db/Metadata/Source/MysqlMetadata.php @@ -0,0 +1,493 @@ +data['schemas'])) { + return; + } + $this->prepareDataHierarchy('schemas'); + + $p = $this->adapter->getPlatform(); + + $sql = 'SELECT ' . $p->quoteIdentifier('SCHEMA_NAME') + . ' FROM ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'SCHEMATA')) + . ' WHERE ' . $p->quoteIdentifier('SCHEMA_NAME') + . ' != \'INFORMATION_SCHEMA\''; + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $schemas = array(); + foreach ($results->toArray() as $row) { + $schemas[] = $row['SCHEMA_NAME']; + } + + $this->data['schemas'] = $schemas; + } + + protected function loadTableNameData($schema) + { + if (isset($this->data['table_names'][$schema])) { + return; + } + $this->prepareDataHierarchy('table_names', $schema); + + $p = $this->adapter->getPlatform(); + + $isColumns = array( + array('T', 'TABLE_NAME'), + array('T', 'TABLE_TYPE'), + array('V', 'VIEW_DEFINITION'), + array('V', 'CHECK_OPTION'), + array('V', 'IS_UPDATABLE'), + ); + + array_walk($isColumns, function (&$c) use ($p) { $c = $p->quoteIdentifierChain($c); }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'TABLES')) . 'T' + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'VIEWS')) . ' V' + . ' ON ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteIdentifierChain(array('V', 'TABLE_SCHEMA')) + . ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_NAME')) + . ' = ' . $p->quoteIdentifierChain(array('V', 'TABLE_NAME')) + + . ' WHERE ' . $p->quoteIdentifierChain(array('T', 'TABLE_TYPE')) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' != \'INFORMATION_SCHEMA\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $tables = array(); + foreach ($results->toArray() as $row) { + $tables[$row['TABLE_NAME']] = array( + 'table_type' => $row['TABLE_TYPE'], + 'view_definition' => $row['VIEW_DEFINITION'], + 'check_option' => $row['CHECK_OPTION'], + 'is_updatable' => ('YES' == $row['IS_UPDATABLE']), + ); + } + + $this->data['table_names'][$schema] = $tables; + } + + protected function loadColumnData($table, $schema) + { + if (isset($this->data['columns'][$schema][$table])) { + return; + } + $this->prepareDataHierarchy('columns', $schema, $table); + $p = $this->adapter->getPlatform(); + + $isColumns = array( + array('C', 'ORDINAL_POSITION'), + array('C', 'COLUMN_DEFAULT'), + array('C', 'IS_NULLABLE'), + array('C', 'DATA_TYPE'), + array('C', 'CHARACTER_MAXIMUM_LENGTH'), + array('C', 'CHARACTER_OCTET_LENGTH'), + array('C', 'NUMERIC_PRECISION'), + array('C', 'NUMERIC_SCALE'), + array('C', 'COLUMN_NAME'), + array('C', 'COLUMN_TYPE'), + ); + + array_walk($isColumns, function (&$c) use ($p) { $c = $p->quoteIdentifierChain($c); }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'TABLES')) . 'T' + . ' INNER JOIN ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'COLUMNS')) . 'C' + . ' ON ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteIdentifierChain(array('C', 'TABLE_SCHEMA')) + . ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_NAME')) + . ' = ' . $p->quoteIdentifierChain(array('C', 'TABLE_NAME')) + . ' WHERE ' . $p->quoteIdentifierChain(array('T', 'TABLE_TYPE')) + . ' IN (\'BASE TABLE\', \'VIEW\')' + . ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_NAME')) + . ' = ' . $p->quoteTrustedValue($table); + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' != \'INFORMATION_SCHEMA\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + $columns = array(); + foreach ($results->toArray() as $row) { + $erratas = array(); + $matches = array(); + if (preg_match('/^(?:enum|set)\((.+)\)$/i', $row['COLUMN_TYPE'], $matches)) { + $permittedValues = $matches[1]; + if (preg_match_all("/\\s*'((?:[^']++|'')*+)'\\s*(?:,|\$)/", $permittedValues, $matches, PREG_PATTERN_ORDER)) { + $permittedValues = str_replace("''", "'", $matches[1]); + } else { + $permittedValues = array($permittedValues); + } + $erratas['permitted_values'] = $permittedValues; + } + $columns[$row['COLUMN_NAME']] = array( + 'ordinal_position' => $row['ORDINAL_POSITION'], + 'column_default' => $row['COLUMN_DEFAULT'], + 'is_nullable' => ('YES' == $row['IS_NULLABLE']), + 'data_type' => $row['DATA_TYPE'], + 'character_maximum_length' => $row['CHARACTER_MAXIMUM_LENGTH'], + 'character_octet_length' => $row['CHARACTER_OCTET_LENGTH'], + 'numeric_precision' => $row['NUMERIC_PRECISION'], + 'numeric_scale' => $row['NUMERIC_SCALE'], + 'numeric_unsigned' => (false !== strpos($row['COLUMN_TYPE'], 'unsigned')), + 'erratas' => $erratas, + ); + } + + $this->data['columns'][$schema][$table] = $columns; + } + + protected function loadConstraintData($table, $schema) + { + if (isset($this->data['constraints'][$schema][$table])) { + return; + } + + $this->prepareDataHierarchy('constraints', $schema, $table); + + $isColumns = array( + array('T', 'TABLE_NAME'), + array('TC', 'CONSTRAINT_NAME'), + array('TC', 'CONSTRAINT_TYPE'), + array('KCU', 'COLUMN_NAME'), + array('RC', 'MATCH_OPTION'), + array('RC', 'UPDATE_RULE'), + array('RC', 'DELETE_RULE'), + array('KCU', 'REFERENCED_TABLE_SCHEMA'), + array('KCU', 'REFERENCED_TABLE_NAME'), + array('KCU', 'REFERENCED_COLUMN_NAME'), + ); + + $p = $this->adapter->getPlatform(); + + array_walk($isColumns, function (&$c) use ($p) { + $c = $p->quoteIdentifierChain($c); + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'TABLES')) . ' T' + + . ' INNER JOIN ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'TABLE_CONSTRAINTS')) . ' TC' + . ' ON ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteIdentifierChain(array('TC', 'TABLE_SCHEMA')) + . ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_NAME')) + . ' = ' . $p->quoteIdentifierChain(array('TC', 'TABLE_NAME')) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'KEY_COLUMN_USAGE')) . ' KCU' + . ' ON ' . $p->quoteIdentifierChain(array('TC', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteIdentifierChain(array('KCU', 'TABLE_SCHEMA')) + . ' AND ' . $p->quoteIdentifierChain(array('TC', 'TABLE_NAME')) + . ' = ' . $p->quoteIdentifierChain(array('KCU', 'TABLE_NAME')) + . ' AND ' . $p->quoteIdentifierChain(array('TC', 'CONSTRAINT_NAME')) + . ' = ' . $p->quoteIdentifierChain(array('KCU', 'CONSTRAINT_NAME')) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'REFERENTIAL_CONSTRAINTS')) . ' RC' + . ' ON ' . $p->quoteIdentifierChain(array('TC', 'CONSTRAINT_SCHEMA')) + . ' = ' . $p->quoteIdentifierChain(array('RC', 'CONSTRAINT_SCHEMA')) + . ' AND ' . $p->quoteIdentifierChain(array('TC', 'CONSTRAINT_NAME')) + . ' = ' . $p->quoteIdentifierChain(array('RC', 'CONSTRAINT_NAME')) + + . ' WHERE ' . $p->quoteIdentifierChain(array('T', 'TABLE_NAME')) + . ' = ' . $p->quoteTrustedValue($table) + . ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_TYPE')) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' != \'INFORMATION_SCHEMA\''; + } + + $sql .= ' ORDER BY CASE ' . $p->quoteIdentifierChain(array('TC', 'CONSTRAINT_TYPE')) + . " WHEN 'PRIMARY KEY' THEN 1" + . " WHEN 'UNIQUE' THEN 2" + . " WHEN 'FOREIGN KEY' THEN 3" + . " ELSE 4 END" + + . ', ' . $p->quoteIdentifierChain(array('TC', 'CONSTRAINT_NAME')) + . ', ' . $p->quoteIdentifierChain(array('KCU', 'ORDINAL_POSITION')); + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $realName = null; + $constraints = array(); + foreach ($results->toArray() as $row) { + if ($row['CONSTRAINT_NAME'] !== $realName) { + $realName = $row['CONSTRAINT_NAME']; + $isFK = ('FOREIGN KEY' == $row['CONSTRAINT_TYPE']); + if ($isFK) { + $name = $realName; + } else { + $name = '_zf_' . $row['TABLE_NAME'] . '_' . $realName; + } + $constraints[$name] = array( + 'constraint_name' => $name, + 'constraint_type' => $row['CONSTRAINT_TYPE'], + 'table_name' => $row['TABLE_NAME'], + 'columns' => array(), + ); + if ($isFK) { + $constraints[$name]['referenced_table_schema'] = $row['REFERENCED_TABLE_SCHEMA']; + $constraints[$name]['referenced_table_name'] = $row['REFERENCED_TABLE_NAME']; + $constraints[$name]['referenced_columns'] = array(); + $constraints[$name]['match_option'] = $row['MATCH_OPTION']; + $constraints[$name]['update_rule'] = $row['UPDATE_RULE']; + $constraints[$name]['delete_rule'] = $row['DELETE_RULE']; + } + } + $constraints[$name]['columns'][] = $row['COLUMN_NAME']; + if ($isFK) { + $constraints[$name]['referenced_columns'][] = $row['REFERENCED_COLUMN_NAME']; + } + } + + $this->data['constraints'][$schema][$table] = $constraints; + } + + protected function loadConstraintDataNames($schema) + { + if (isset($this->data['constraint_names'][$schema])) { + return; + } + + $this->prepareDataHierarchy('constraint_names', $schema); + + $p = $this->adapter->getPlatform(); + + $isColumns = array( + array('TC', 'TABLE_NAME'), + array('TC', 'CONSTRAINT_NAME'), + array('TC', 'CONSTRAINT_TYPE'), + ); + + array_walk($isColumns, function (&$c) use ($p) { + $c = $p->quoteIdentifierChain($c); + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'TABLES')) . 'T' + . ' INNER JOIN ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'TABLE_CONSTRAINTS')) . 'TC' + . ' ON ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteIdentifierChain(array('TC', 'TABLE_SCHEMA')) + . ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_NAME')) + . ' = ' . $p->quoteIdentifierChain(array('TC', 'TABLE_NAME')) + . ' WHERE ' . $p->quoteIdentifierChain(array('T', 'TABLE_TYPE')) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' != \'INFORMATION_SCHEMA\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $data = array(); + foreach ($results->toArray() as $row) { + $data[] = array_change_key_case($row, CASE_LOWER); + } + + $this->data['constraint_names'][$schema] = $data; + } + + protected function loadConstraintDataKeys($schema) + { + if (isset($this->data['constraint_keys'][$schema])) { + return; + } + + $this->prepareDataHierarchy('constraint_keys', $schema); + + $p = $this->adapter->getPlatform(); + + $isColumns = array( + array('T', 'TABLE_NAME'), + array('KCU', 'CONSTRAINT_NAME'), + array('KCU', 'COLUMN_NAME'), + array('KCU', 'ORDINAL_POSITION'), + ); + + array_walk($isColumns, function (&$c) use ($p) { + $c = $p->quoteIdentifierChain($c); + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'TABLES')) . 'T' + + . ' INNER JOIN ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'KEY_COLUMN_USAGE')) . 'KCU' + . ' ON ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteIdentifierChain(array('KCU', 'TABLE_SCHEMA')) + . ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_NAME')) + . ' = ' . $p->quoteIdentifierChain(array('KCU', 'TABLE_NAME')) + + . ' WHERE ' . $p->quoteIdentifierChain(array('T', 'TABLE_TYPE')) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' != \'INFORMATION_SCHEMA\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $data = array(); + foreach ($results->toArray() as $row) { + $data[] = array_change_key_case($row, CASE_LOWER); + } + + $this->data['constraint_keys'][$schema] = $data; + } + + protected function loadConstraintReferences($table, $schema) + { + parent::loadConstraintReferences($table, $schema); + + $p = $this->adapter->getPlatform(); + + $isColumns = array( + array('RC', 'TABLE_NAME'), + array('RC', 'CONSTRAINT_NAME'), + array('RC', 'UPDATE_RULE'), + array('RC', 'DELETE_RULE'), + array('KCU', 'REFERENCED_TABLE_SCHEMA'), + array('KCU', 'REFERENCED_TABLE_NAME'), + array('KCU', 'REFERENCED_COLUMN_NAME'), + ); + + array_walk($isColumns, function (&$c) use ($p) { + $c = $p->quoteIdentifierChain($c); + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . 'FROM ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'TABLES')) . 'T' + + . ' INNER JOIN ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'REFERENTIAL_CONSTRAINTS')) . 'RC' + . ' ON ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteIdentifierChain(array('RC', 'CONSTRAINT_SCHEMA')) + . ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_NAME')) + . ' = ' . $p->quoteIdentifierChain(array('RC', 'TABLE_NAME')) + + . ' INNER JOIN ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'KEY_COLUMN_USAGE')) . 'KCU' + . ' ON ' . $p->quoteIdentifierChain(array('RC', 'CONSTRAINT_SCHEMA')) + . ' = ' . $p->quoteIdentifierChain(array('KCU', 'TABLE_SCHEMA')) + . ' AND ' . $p->quoteIdentifierChain(array('RC', 'TABLE_NAME')) + . ' = ' . $p->quoteIdentifierChain(array('KCU', 'TABLE_NAME')) + . ' AND ' . $p->quoteIdentifierChain(array('RC', 'CONSTRAINT_NAME')) + . ' = ' . $p->quoteIdentifierChain(array('KCU', 'CONSTRAINT_NAME')) + + . 'WHERE ' . $p->quoteIdentifierChain(array('T', 'TABLE_TYPE')) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' != \'INFORMATION_SCHEMA\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $data = array(); + foreach ($results->toArray() as $row) { + $data[] = array_change_key_case($row, CASE_LOWER); + } + + $this->data['constraint_references'][$schema] = $data; + } + + protected function loadTriggerData($schema) + { + if (isset($this->data['triggers'][$schema])) { + return; + } + + $this->prepareDataHierarchy('triggers', $schema); + + $p = $this->adapter->getPlatform(); + + $isColumns = array( +// 'TRIGGER_CATALOG', +// 'TRIGGER_SCHEMA', + 'TRIGGER_NAME', + 'EVENT_MANIPULATION', + 'EVENT_OBJECT_CATALOG', + 'EVENT_OBJECT_SCHEMA', + 'EVENT_OBJECT_TABLE', + 'ACTION_ORDER', + 'ACTION_CONDITION', + 'ACTION_STATEMENT', + 'ACTION_ORIENTATION', + 'ACTION_TIMING', + 'ACTION_REFERENCE_OLD_TABLE', + 'ACTION_REFERENCE_NEW_TABLE', + 'ACTION_REFERENCE_OLD_ROW', + 'ACTION_REFERENCE_NEW_ROW', + 'CREATED', + ); + + array_walk($isColumns, function (&$c) use ($p) { + $c = $p->quoteIdentifier($c); + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'TRIGGERS')) + . ' WHERE '; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= $p->quoteIdentifier('TRIGGER_SCHEMA') + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= $p->quoteIdentifier('TRIGGER_SCHEMA') + . ' != \'INFORMATION_SCHEMA\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $data = array(); + foreach ($results->toArray() as $row) { + $row = array_change_key_case($row, CASE_LOWER); + if (null !== $row['created']) { + $row['created'] = new \DateTime($row['created']); + } + $data[$row['trigger_name']] = $row; + } + + $this->data['triggers'][$schema] = $data; + } +} diff --git a/library/Zend/Db/Metadata/Source/OracleMetadata.php b/library/Zend/Db/Metadata/Source/OracleMetadata.php new file mode 100755 index 0000000000..44deac13d2 --- /dev/null +++ b/library/Zend/Db/Metadata/Source/OracleMetadata.php @@ -0,0 +1,256 @@ + 'CHECK', + 'P' => 'PRIMARY KEY', + 'R' => 'FOREIGN_KEY' + ); + + /** + * {@inheritdoc} + * @see \Zend\Db\Metadata\Source\AbstractSource::loadColumnData() + */ + protected function loadColumnData($table, $schema) + { + if (isset($this->data['columns'][$schema][$table])) { + return; + } + + $isColumns = array( + 'COLUMN_ID', + 'COLUMN_NAME', + 'DATA_DEFAULT', + 'NULLABLE', + 'DATA_TYPE', + 'DATA_LENGTH', + 'DATA_PRECISION', + 'DATA_SCALE' + ); + + $this->prepareDataHierarchy('columns', $schema, $table); + $parameters = array( + ':ownername' => $schema, + ':tablename' => $table + ); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM all_tab_columns' + . ' WHERE owner = :ownername AND table_name = :tablename'; + + $result = $this->adapter->query($sql)->execute($parameters); + $columns = array(); + + foreach ($result as $row) { + $columns[$row['COLUMN_NAME']] = array( + 'ordinal_position' => $row['COLUMN_ID'], + 'column_default' => $row['DATA_DEFAULT'], + 'is_nullable' => ('Y' == $row['NULLABLE']), + 'data_type' => $row['DATA_TYPE'], + 'character_maximum_length' => $row['DATA_LENGTH'], + 'character_octet_length' => null, + 'numeric_precision' => $row['DATA_PRECISION'], + 'numeric_scale' => $row['DATA_SCALE'], + 'numeric_unsigned' => false, + 'erratas' => array(), + ); + } + + $this->data['columns'][$schema][$table] = $columns; + return $this; + } + + /** + * Constraint type + * + * @param string $type + * @return string + */ + protected function getConstraintType($type) + { + if (isset($this->constraintTypeMap[$type])) { + return $this->constraintTypeMap[$type]; + } + + return $type; + } + + /** + * {@inheritdoc} + * @see \Zend\Db\Metadata\Source\AbstractSource::loadConstraintData() + */ + protected function loadConstraintData($table, $schema) + { + if (isset($this->data['constraints'][$schema][$table])) { + return; + } + + $this->prepareDataHierarchy('constraints', $schema, $table); + $sql = ' + SELECT + ac.owner, + ac.constraint_name, + ac.constraint_type, + ac.search_condition check_clause, + ac.table_name, + ac.delete_rule, + cc1.column_name, + cc2.table_name as ref_table, + cc2.column_name as ref_column, + cc2.owner as ref_owner + FROM all_constraints ac + INNER JOIN all_cons_columns cc1 + ON cc1.constraint_name = ac.constraint_name + LEFT JOIN all_cons_columns cc2 + ON cc2.constraint_name = ac.r_constraint_name + AND cc2.position = cc1.position + + WHERE + ac.owner = :schema AND ac.table_name = :table + + ORDER BY ac.constraint_name; + '; + + $parameters = array( + ':schema' => $schema, + ':table' => $table + ); + + $results = $this->adapter->query($sql)->execute($parameters); + $isFK = false; + $name = null; + $constraints = array(); + + foreach ($results as $row) { + if ($row['CONSTRAINT_NAME'] !== $name) { + $name = $row['CONSTRAINT_NAME']; + $constraints[$name] = array( + 'constraint_name' => $name, + 'constraint_type' => $this->getConstraintType($row['CONSTRAINT_TYPE']), + 'table_name' => $row['TABLE_NAME'], + ); + + if ('C' == $row['CONSTRAINT_TYPE']) { + $constraints[$name]['CHECK_CLAUSE'] = $row['CHECK_CLAUSE']; + continue; + } + + $constraints[$name]['columns'] = array(); + + $isFK = ('R' == $row['CONSTRAINT_TYPE']); + if ($isFK) { + $constraints[$name]['referenced_table_schema'] = $row['REF_OWNER']; + $constraints[$name]['referenced_table_name'] = $row['REF_TABLE']; + $constraints[$name]['referenced_columns'] = array(); + $constraints[$name]['match_option'] = 'NONE'; + $constraints[$name]['update_rule'] = null; + $constraints[$name]['delete_rule'] = $row['DELETE_RULE']; + } + } + + $constraints[$name]['columns'][] = $row['COLUMN_NAME']; + if ($isFK) { + $constraints[$name]['referenced_columns'][] = $row['REF_COLUMN']; + } + } + + return $this; + } + + /** + * {@inheritdoc} + * @see \Zend\Db\Metadata\Source\AbstractSource::loadSchemaData() + */ + protected function loadSchemaData() + { + if (isset($this->data['schemas'])) { + return; + } + + $this->prepareDataHierarchy('schemas'); + $sql = 'SELECT USERNAME FROM ALL_USERS'; + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $schemas = array(); + foreach ($results->toArray() as $row) { + $schemas[] = $row['USERNAME']; + } + + $this->data['schemas'] = $schemas; + } + + /** + * {@inheritdoc} + * @see \Zend\Db\Metadata\Source\AbstractSource::loadTableNameData() + */ + protected function loadTableNameData($schema) + { + if (isset($this->data['table_names'][$schema])) { + return $this; + } + + $this->prepareDataHierarchy('table_names', $schema); + $tables = array(); + + // Tables + $bind = array(':OWNER' => strtoupper($schema)); + $result = $this->adapter->query('SELECT TABLE_NAME FROM ALL_TABLES WHERE OWNER=:OWNER')->execute($bind); + + foreach ($result as $row) { + $tables[$row['TABLE_NAME']] = array( + 'table_type' => 'BASE TABLE', + 'view_definition' => null, + 'check_option' => null, + 'is_updatable' => false, + ); + } + + // Views + $result = $this->adapter->query('SELECT VIEW_NAME, TEXT FROM ALL_VIEWS WHERE OWNER=:OWNER', $bind); + foreach ($result as $row) { + $tables[$row['VIEW_NAME']] = array( + 'table_type' => 'VIEW', + 'view_definition' => null, + 'check_option' => 'NONE', + 'is_updatable' => false, + ); + } + + $this->data['table_names'][$schema] = $tables; + return $this; + } + + /** + * FIXME: load trigger data + * + * {@inheritdoc} + * + * @see \Zend\Db\Metadata\Source\AbstractSource::loadTriggerData() + */ + protected function loadTriggerData($schema) + { + if (isset($this->data['triggers'][$schema])) { + return; + } + + $this->prepareDataHierarchy('triggers', $schema); + } +} diff --git a/library/Zend/Db/Metadata/Source/PostgresqlMetadata.php b/library/Zend/Db/Metadata/Source/PostgresqlMetadata.php new file mode 100755 index 0000000000..bb274487db --- /dev/null +++ b/library/Zend/Db/Metadata/Source/PostgresqlMetadata.php @@ -0,0 +1,345 @@ +data['schemas'])) { + return; + } + $this->prepareDataHierarchy('schemas'); + + $p = $this->adapter->getPlatform(); + + $sql = 'SELECT ' . $p->quoteIdentifier('schema_name') + . ' FROM ' . $p->quoteIdentifierChain(array('information_schema', 'schemata')) + . ' WHERE ' . $p->quoteIdentifier('schema_name') + . ' != \'information_schema\'' + . ' AND ' . $p->quoteIdentifier('schema_name') . " NOT LIKE 'pg_%'"; + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $schemas = array(); + foreach ($results->toArray() as $row) { + $schemas[] = $row['schema_name']; + } + + $this->data['schemas'] = $schemas; + } + + protected function loadTableNameData($schema) + { + if (isset($this->data['table_names'][$schema])) { + return; + } + $this->prepareDataHierarchy('table_names', $schema); + + $p = $this->adapter->getPlatform(); + + $isColumns = array( + array('t', 'table_name'), + array('t', 'table_type'), + array('v', 'view_definition'), + array('v', 'check_option'), + array('v', 'is_updatable'), + ); + + array_walk($isColumns, function (&$c) use ($p) { $c = $p->quoteIdentifierChain($c); }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(array('information_schema', 'tables')) . ' t' + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(array('information_schema', 'views')) . ' v' + . ' ON ' . $p->quoteIdentifierChain(array('t', 'table_schema')) + . ' = ' . $p->quoteIdentifierChain(array('v', 'table_schema')) + . ' AND ' . $p->quoteIdentifierChain(array('t', 'table_name')) + . ' = ' . $p->quoteIdentifierChain(array('v', 'table_name')) + + . ' WHERE ' . $p->quoteIdentifierChain(array('t', 'table_type')) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('t', 'table_schema')) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('t', 'table_schema')) + . ' != \'information_schema\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $tables = array(); + foreach ($results->toArray() as $row) { + $tables[$row['table_name']] = array( + 'table_type' => $row['table_type'], + 'view_definition' => $row['view_definition'], + 'check_option' => $row['check_option'], + 'is_updatable' => ('YES' == $row['is_updatable']), + ); + } + + $this->data['table_names'][$schema] = $tables; + } + + protected function loadColumnData($table, $schema) + { + if (isset($this->data['columns'][$schema][$table])) { + return; + } + + $this->prepareDataHierarchy('columns', $schema, $table); + + $platform = $this->adapter->getPlatform(); + + $isColumns = array( + 'table_name', + 'column_name', + 'ordinal_position', + 'column_default', + 'is_nullable', + 'data_type', + 'character_maximum_length', + 'character_octet_length', + 'numeric_precision', + 'numeric_scale', + ); + + array_walk($isColumns, function (&$c) use ($platform) { $c = $platform->quoteIdentifier($c); }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $platform->quoteIdentifier('information_schema') + . $platform->getIdentifierSeparator() . $platform->quoteIdentifier('columns') + . ' WHERE ' . $platform->quoteIdentifier('table_schema') + . ' != \'information\'' + . ' AND ' . $platform->quoteIdentifier('table_name') + . ' = ' . $platform->quoteTrustedValue($table); + + if ($schema != '__DEFAULT_SCHEMA__') { + $sql .= ' AND ' . $platform->quoteIdentifier('table_schema') + . ' = ' . $platform->quoteTrustedValue($schema); + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + $columns = array(); + foreach ($results->toArray() as $row) { + $columns[$row['column_name']] = array( + 'ordinal_position' => $row['ordinal_position'], + 'column_default' => $row['column_default'], + 'is_nullable' => ('YES' == $row['is_nullable']), + 'data_type' => $row['data_type'], + 'character_maximum_length' => $row['character_maximum_length'], + 'character_octet_length' => $row['character_octet_length'], + 'numeric_precision' => $row['numeric_precision'], + 'numeric_scale' => $row['numeric_scale'], + 'numeric_unsigned' => null, + 'erratas' => array(), + ); + } + + $this->data['columns'][$schema][$table] = $columns; + } + + protected function loadConstraintData($table, $schema) + { + if (isset($this->data['constraints'][$schema][$table])) { + return; + } + + $this->prepareDataHierarchy('constraints', $schema, $table); + + $isColumns = array( + array('t', 'table_name'), + array('tc', 'constraint_name'), + array('tc', 'constraint_type'), + array('kcu', 'column_name'), + array('cc', 'check_clause'), + array('rc', 'match_option'), + array('rc', 'update_rule'), + array('rc', 'delete_rule'), + array('referenced_table_schema' => 'kcu2', 'table_schema'), + array('referenced_table_name' => 'kcu2', 'table_name'), + array('referenced_column_name' => 'kcu2', 'column_name'), + ); + + $p = $this->adapter->getPlatform(); + + array_walk($isColumns, function (&$c) use ($p) { + $alias = key($c); + $c = $p->quoteIdentifierChain($c); + if (is_string($alias)) { + $c .= ' ' . $p->quoteIdentifier($alias); + } + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(array('information_schema', 'tables')) . ' t' + + . ' INNER JOIN ' . $p->quoteIdentifierChain(array('information_schema', 'table_constraints')) . ' tc' + . ' ON ' . $p->quoteIdentifierChain(array('t', 'table_schema')) + . ' = ' . $p->quoteIdentifierChain(array('tc', 'table_schema')) + . ' AND ' . $p->quoteIdentifierChain(array('t', 'table_name')) + . ' = ' . $p->quoteIdentifierChain(array('tc', 'table_name')) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(array('information_schema', 'key_column_usage')) . ' kcu' + . ' ON ' . $p->quoteIdentifierChain(array('tc', 'table_schema')) + . ' = ' . $p->quoteIdentifierChain(array('kcu', 'table_schema')) + . ' AND ' . $p->quoteIdentifierChain(array('tc', 'table_name')) + . ' = ' . $p->quoteIdentifierChain(array('kcu', 'table_name')) + . ' AND ' . $p->quoteIdentifierChain(array('tc', 'constraint_name')) + . ' = ' . $p->quoteIdentifierChain(array('kcu', 'constraint_name')) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(array('information_schema', 'check_constraints')) . ' cc' + . ' ON ' . $p->quoteIdentifierChain(array('tc', 'constraint_schema')) + . ' = ' . $p->quoteIdentifierChain(array('cc', 'constraint_schema')) + . ' AND ' . $p->quoteIdentifierChain(array('tc', 'constraint_name')) + . ' = ' . $p->quoteIdentifierChain(array('cc', 'constraint_name')) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(array('information_schema', 'referential_constraints')) . ' rc' + . ' ON ' . $p->quoteIdentifierChain(array('tc', 'constraint_schema')) + . ' = ' . $p->quoteIdentifierChain(array('rc', 'constraint_schema')) + . ' AND ' . $p->quoteIdentifierChain(array('tc', 'constraint_name')) + . ' = ' . $p->quoteIdentifierChain(array('rc', 'constraint_name')) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(array('information_schema', 'key_column_usage')) . ' kcu2' + . ' ON ' . $p->quoteIdentifierChain(array('rc', 'unique_constraint_schema')) + . ' = ' . $p->quoteIdentifierChain(array('kcu2', 'constraint_schema')) + . ' AND ' . $p->quoteIdentifierChain(array('rc', 'unique_constraint_name')) + . ' = ' . $p->quoteIdentifierChain(array('kcu2', 'constraint_name')) + . ' AND ' . $p->quoteIdentifierChain(array('kcu', 'position_in_unique_constraint')) + . ' = ' . $p->quoteIdentifierChain(array('kcu2', 'ordinal_position')) + + . ' WHERE ' . $p->quoteIdentifierChain(array('t', 'table_name')) + . ' = ' . $p->quoteTrustedValue($table) + . ' AND ' . $p->quoteIdentifierChain(array('t', 'table_type')) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('t', 'table_schema')) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('t', 'table_schema')) + . ' != \'information_schema\''; + } + + $sql .= ' ORDER BY CASE ' . $p->quoteIdentifierChain(array('tc', 'constraint_type')) + . " WHEN 'PRIMARY KEY' THEN 1" + . " WHEN 'UNIQUE' THEN 2" + . " WHEN 'FOREIGN KEY' THEN 3" + . " WHEN 'CHECK' THEN 4" + . " ELSE 5 END" + . ', ' . $p->quoteIdentifierChain(array('tc', 'constraint_name')) + . ', ' . $p->quoteIdentifierChain(array('kcu', 'ordinal_position')); + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $name = null; + $constraints = array(); + foreach ($results->toArray() as $row) { + if ($row['constraint_name'] !== $name) { + $name = $row['constraint_name']; + $constraints[$name] = array( + 'constraint_name' => $name, + 'constraint_type' => $row['constraint_type'], + 'table_name' => $row['table_name'], + ); + if ('CHECK' == $row['constraint_type']) { + $constraints[$name]['check_clause'] = $row['check_clause']; + continue; + } + $constraints[$name]['columns'] = array(); + $isFK = ('FOREIGN KEY' == $row['constraint_type']); + if ($isFK) { + $constraints[$name]['referenced_table_schema'] = $row['referenced_table_schema']; + $constraints[$name]['referenced_table_name'] = $row['referenced_table_name']; + $constraints[$name]['referenced_columns'] = array(); + $constraints[$name]['match_option'] = $row['match_option']; + $constraints[$name]['update_rule'] = $row['update_rule']; + $constraints[$name]['delete_rule'] = $row['delete_rule']; + } + } + $constraints[$name]['columns'][] = $row['column_name']; + if ($isFK) { + $constraints[$name]['referenced_columns'][] = $row['referenced_column_name']; + } + } + + $this->data['constraints'][$schema][$table] = $constraints; + } + + protected function loadTriggerData($schema) + { + if (isset($this->data['triggers'][$schema])) { + return; + } + + $this->prepareDataHierarchy('triggers', $schema); + + $p = $this->adapter->getPlatform(); + + $isColumns = array( + 'trigger_name', + 'event_manipulation', + 'event_object_catalog', + 'event_object_schema', + 'event_object_table', + 'action_order', + 'action_condition', + 'action_statement', + 'action_orientation', + array('action_timing' => 'condition_timing'), + array('action_reference_old_table' => 'condition_reference_old_table'), + array('action_reference_new_table' => 'condition_reference_new_table'), + 'created', + ); + + array_walk($isColumns, function (&$c) use ($p) { + if (is_array($c)) { + $alias = key($c); + $c = $p->quoteIdentifierChain($c); + if (is_string($alias)) { + $c .= ' ' . $p->quoteIdentifier($alias); + } + } else { + $c = $p->quoteIdentifier($c); + } + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(array('information_schema', 'triggers')) + . ' WHERE '; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= $p->quoteIdentifier('trigger_schema') + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= $p->quoteIdentifier('trigger_schema') + . ' != \'information_schema\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $data = array(); + foreach ($results->toArray() as $row) { + $row = array_change_key_case($row, CASE_LOWER); + $row['action_reference_old_row'] = 'OLD'; + $row['action_reference_new_row'] = 'NEW'; + if (null !== $row['created']) { + $row['created'] = new \DateTime($row['created']); + } + $data[$row['trigger_name']] = $row; + } + + $this->data['triggers'][$schema] = $data; + } +} diff --git a/library/Zend/Db/Metadata/Source/SqlServerMetadata.php b/library/Zend/Db/Metadata/Source/SqlServerMetadata.php new file mode 100755 index 0000000000..b2b3e76fab --- /dev/null +++ b/library/Zend/Db/Metadata/Source/SqlServerMetadata.php @@ -0,0 +1,341 @@ +data['schemas'])) { + return; + } + $this->prepareDataHierarchy('schemas'); + + $p = $this->adapter->getPlatform(); + + $sql = 'SELECT ' . $p->quoteIdentifier('SCHEMA_NAME') + . ' FROM ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'SCHEMATA')) + . ' WHERE ' . $p->quoteIdentifier('SCHEMA_NAME') + . ' != \'INFORMATION_SCHEMA\''; + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $schemas = array(); + foreach ($results->toArray() as $row) { + $schemas[] = $row['SCHEMA_NAME']; + } + + $this->data['schemas'] = $schemas; + } + + protected function loadTableNameData($schema) + { + if (isset($this->data['table_names'][$schema])) { + return; + } + $this->prepareDataHierarchy('table_names', $schema); + + $p = $this->adapter->getPlatform(); + + $isColumns = array( + array('T', 'TABLE_NAME'), + array('T', 'TABLE_TYPE'), + array('V', 'VIEW_DEFINITION'), + array('V', 'CHECK_OPTION'), + array('V', 'IS_UPDATABLE'), + ); + + array_walk($isColumns, function (&$c) use ($p) { $c = $p->quoteIdentifierChain($c); }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'TABLES')) . ' t' + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'VIEWS')) . ' v' + . ' ON ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteIdentifierChain(array('V', 'TABLE_SCHEMA')) + . ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_NAME')) + . ' = ' . $p->quoteIdentifierChain(array('V', 'TABLE_NAME')) + + . ' WHERE ' . $p->quoteIdentifierChain(array('T', 'TABLE_TYPE')) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' != \'INFORMATION_SCHEMA\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $tables = array(); + foreach ($results->toArray() as $row) { + $tables[$row['TABLE_NAME']] = array( + 'table_type' => $row['TABLE_TYPE'], + 'view_definition' => $row['VIEW_DEFINITION'], + 'check_option' => $row['CHECK_OPTION'], + 'is_updatable' => ('YES' == $row['IS_UPDATABLE']), + ); + } + + $this->data['table_names'][$schema] = $tables; + } + + protected function loadColumnData($table, $schema) + { + if (isset($this->data['columns'][$schema][$table])) { + return; + } + $this->prepareDataHierarchy('columns', $schema, $table); + $p = $this->adapter->getPlatform(); + + $isColumns = array( + array('C', 'ORDINAL_POSITION'), + array('C', 'COLUMN_DEFAULT'), + array('C', 'IS_NULLABLE'), + array('C', 'DATA_TYPE'), + array('C', 'CHARACTER_MAXIMUM_LENGTH'), + array('C', 'CHARACTER_OCTET_LENGTH'), + array('C', 'NUMERIC_PRECISION'), + array('C', 'NUMERIC_SCALE'), + array('C', 'COLUMN_NAME'), + ); + + array_walk($isColumns, function (&$c) use ($p) { $c = $p->quoteIdentifierChain($c); }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'TABLES')) . 'T' + . ' INNER JOIN ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'COLUMNS')) . 'C' + . ' ON ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteIdentifierChain(array('C', 'TABLE_SCHEMA')) + . ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_NAME')) + . ' = ' . $p->quoteIdentifierChain(array('C', 'TABLE_NAME')) + . ' WHERE ' . $p->quoteIdentifierChain(array('T', 'TABLE_TYPE')) + . ' IN (\'BASE TABLE\', \'VIEW\')' + . ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_NAME')) + . ' = ' . $p->quoteTrustedValue($table); + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' != \'INFORMATION_SCHEMA\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + $columns = array(); + foreach ($results->toArray() as $row) { + $columns[$row['COLUMN_NAME']] = array( + 'ordinal_position' => $row['ORDINAL_POSITION'], + 'column_default' => $row['COLUMN_DEFAULT'], + 'is_nullable' => ('YES' == $row['IS_NULLABLE']), + 'data_type' => $row['DATA_TYPE'], + 'character_maximum_length' => $row['CHARACTER_MAXIMUM_LENGTH'], + 'character_octet_length' => $row['CHARACTER_OCTET_LENGTH'], + 'numeric_precision' => $row['NUMERIC_PRECISION'], + 'numeric_scale' => $row['NUMERIC_SCALE'], + 'numeric_unsigned' => null, + 'erratas' => array(), + ); + } + + $this->data['columns'][$schema][$table] = $columns; + } + + protected function loadConstraintData($table, $schema) + { + if (isset($this->data['constraints'][$schema][$table])) { + return; + } + + $this->prepareDataHierarchy('constraints', $schema, $table); + + $isColumns = array( + array('T', 'TABLE_NAME'), + array('TC', 'CONSTRAINT_NAME'), + array('TC', 'CONSTRAINT_TYPE'), + array('KCU', 'COLUMN_NAME'), + array('CC', 'CHECK_CLAUSE'), + array('RC', 'MATCH_OPTION'), + array('RC', 'UPDATE_RULE'), + array('RC', 'DELETE_RULE'), + array('REFERENCED_TABLE_SCHEMA' => 'KCU2', 'TABLE_SCHEMA'), + array('REFERENCED_TABLE_NAME' => 'KCU2', 'TABLE_NAME'), + array('REFERENCED_COLUMN_NAME' => 'KCU2', 'COLUMN_NAME'), + ); + + $p = $this->adapter->getPlatform(); + + array_walk($isColumns, function (&$c) use ($p) { + $alias = key($c); + $c = $p->quoteIdentifierChain($c); + if (is_string($alias)) { + $c .= ' ' . $p->quoteIdentifier($alias); + } + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'TABLES')) . ' T' + + . ' INNER JOIN ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'TABLE_CONSTRAINTS')) . ' TC' + . ' ON ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteIdentifierChain(array('TC', 'TABLE_SCHEMA')) + . ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_NAME')) + . ' = ' . $p->quoteIdentifierChain(array('TC', 'TABLE_NAME')) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'KEY_COLUMN_USAGE')) . ' KCU' + . ' ON ' . $p->quoteIdentifierChain(array('TC', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteIdentifierChain(array('KCU', 'TABLE_SCHEMA')) + . ' AND ' . $p->quoteIdentifierChain(array('TC', 'TABLE_NAME')) + . ' = ' . $p->quoteIdentifierChain(array('KCU', 'TABLE_NAME')) + . ' AND ' . $p->quoteIdentifierChain(array('TC', 'CONSTRAINT_NAME')) + . ' = ' . $p->quoteIdentifierChain(array('KCU', 'CONSTRAINT_NAME')) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'CHECK_CONSTRAINTS')) . ' CC' + . ' ON ' . $p->quoteIdentifierChain(array('TC', 'CONSTRAINT_SCHEMA')) + . ' = ' . $p->quoteIdentifierChain(array('CC', 'CONSTRAINT_SCHEMA')) + . ' AND ' . $p->quoteIdentifierChain(array('TC', 'CONSTRAINT_NAME')) + . ' = ' . $p->quoteIdentifierChain(array('CC', 'CONSTRAINT_NAME')) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'REFERENTIAL_CONSTRAINTS')) . ' RC' + . ' ON ' . $p->quoteIdentifierChain(array('TC', 'CONSTRAINT_SCHEMA')) + . ' = ' . $p->quoteIdentifierChain(array('RC', 'CONSTRAINT_SCHEMA')) + . ' AND ' . $p->quoteIdentifierChain(array('TC', 'CONSTRAINT_NAME')) + . ' = ' . $p->quoteIdentifierChain(array('RC', 'CONSTRAINT_NAME')) + + . ' LEFT JOIN ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'KEY_COLUMN_USAGE')) . ' KCU2' + . ' ON ' . $p->quoteIdentifierChain(array('RC', 'UNIQUE_CONSTRAINT_SCHEMA')) + . ' = ' . $p->quoteIdentifierChain(array('KCU2', 'CONSTRAINT_SCHEMA')) + . ' AND ' . $p->quoteIdentifierChain(array('RC', 'UNIQUE_CONSTRAINT_NAME')) + . ' = ' . $p->quoteIdentifierChain(array('KCU2', 'CONSTRAINT_NAME')) + . ' AND ' . $p->quoteIdentifierChain(array('KCU', 'ORDINAL_POSITION')) + . ' = ' . $p->quoteIdentifierChain(array('KCU2', 'ORDINAL_POSITION')) + + . ' WHERE ' . $p->quoteIdentifierChain(array('T', 'TABLE_NAME')) + . ' = ' . $p->quoteTrustedValue($table) + . ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_TYPE')) + . ' IN (\'BASE TABLE\', \'VIEW\')'; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= ' AND ' . $p->quoteIdentifierChain(array('T', 'TABLE_SCHEMA')) + . ' != \'INFORMATION_SCHEMA\''; + } + + $sql .= ' ORDER BY CASE ' . $p->quoteIdentifierChain(array('TC', 'CONSTRAINT_TYPE')) + . " WHEN 'PRIMARY KEY' THEN 1" + . " WHEN 'UNIQUE' THEN 2" + . " WHEN 'FOREIGN KEY' THEN 3" + . " WHEN 'CHECK' THEN 4" + . " ELSE 5 END" + . ', ' . $p->quoteIdentifierChain(array('TC', 'CONSTRAINT_NAME')) + . ', ' . $p->quoteIdentifierChain(array('KCU', 'ORDINAL_POSITION')); + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $name = null; + $constraints = array(); + $isFK = false; + foreach ($results->toArray() as $row) { + if ($row['CONSTRAINT_NAME'] !== $name) { + $name = $row['CONSTRAINT_NAME']; + $constraints[$name] = array( + 'constraint_name' => $name, + 'constraint_type' => $row['CONSTRAINT_TYPE'], + 'table_name' => $row['TABLE_NAME'], + ); + if ('CHECK' == $row['CONSTRAINT_TYPE']) { + $constraints[$name]['check_clause'] = $row['CHECK_CLAUSE']; + continue; + } + $constraints[$name]['columns'] = array(); + $isFK = ('FOREIGN KEY' == $row['CONSTRAINT_TYPE']); + if ($isFK) { + $constraints[$name]['referenced_table_schema'] = $row['REFERENCED_TABLE_SCHEMA']; + $constraints[$name]['referenced_table_name'] = $row['REFERENCED_TABLE_NAME']; + $constraints[$name]['referenced_columns'] = array(); + $constraints[$name]['match_option'] = $row['MATCH_OPTION']; + $constraints[$name]['update_rule'] = $row['UPDATE_RULE']; + $constraints[$name]['delete_rule'] = $row['DELETE_RULE']; + } + } + $constraints[$name]['columns'][] = $row['COLUMN_NAME']; + if ($isFK) { + $constraints[$name]['referenced_columns'][] = $row['REFERENCED_COLUMN_NAME']; + } + } + + $this->data['constraints'][$schema][$table] = $constraints; + } + + protected function loadTriggerData($schema) + { + if (isset($this->data['triggers'][$schema])) { + return; + } + + $this->prepareDataHierarchy('triggers', $schema); + + $p = $this->adapter->getPlatform(); + + $isColumns = array( + 'TRIGGER_NAME', + 'EVENT_MANIPULATION', + 'EVENT_OBJECT_CATALOG', + 'EVENT_OBJECT_SCHEMA', + 'EVENT_OBJECT_TABLE', + 'ACTION_ORDER', + 'ACTION_CONDITION', + 'ACTION_STATEMENT', + 'ACTION_ORIENTATION', + 'ACTION_TIMING', + 'ACTION_REFERENCE_OLD_TABLE', + 'ACTION_REFERENCE_NEW_TABLE', + 'ACTION_REFERENCE_OLD_ROW', + 'ACTION_REFERENCE_NEW_ROW', + 'CREATED', + ); + + array_walk($isColumns, function (&$c) use ($p) { + $c = $p->quoteIdentifier($c); + }); + + $sql = 'SELECT ' . implode(', ', $isColumns) + . ' FROM ' . $p->quoteIdentifierChain(array('INFORMATION_SCHEMA', 'TRIGGERS')) + . ' WHERE '; + + if ($schema != self::DEFAULT_SCHEMA) { + $sql .= $p->quoteIdentifier('TRIGGER_SCHEMA') + . ' = ' . $p->quoteTrustedValue($schema); + } else { + $sql .= $p->quoteIdentifier('TRIGGER_SCHEMA') + . ' != \'INFORMATION_SCHEMA\''; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + + $data = array(); + foreach ($results->toArray() as $row) { + $row = array_change_key_case($row, CASE_LOWER); + if (null !== $row['created']) { + $row['created'] = new \DateTime($row['created']); + } + $data[$row['trigger_name']] = $row; + } + + $this->data['triggers'][$schema] = $data; + } +} diff --git a/library/Zend/Db/Metadata/Source/SqliteMetadata.php b/library/Zend/Db/Metadata/Source/SqliteMetadata.php new file mode 100755 index 0000000000..f3869af8ca --- /dev/null +++ b/library/Zend/Db/Metadata/Source/SqliteMetadata.php @@ -0,0 +1,390 @@ +data['schemas'])) { + return; + } + $this->prepareDataHierarchy('schemas'); + + $results = $this->fetchPragma('database_list'); + foreach ($results as $row) { + $schemas[] = $row['name']; + } + $this->data['schemas'] = $schemas; + } + + protected function loadTableNameData($schema) + { + if (isset($this->data['table_names'][$schema])) { + return; + } + $this->prepareDataHierarchy('table_names', $schema); + + // FEATURE: Filename? + + $p = $this->adapter->getPlatform(); + + $sql = 'SELECT "name", "type", "sql" FROM ' . $p->quoteIdentifierChain(array($schema, 'sqlite_master')) + . ' WHERE "type" IN (\'table\',\'view\') AND "name" NOT LIKE \'sqlite_%\''; + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + $tables = array(); + foreach ($results->toArray() as $row) { + if ('table' == $row['type']) { + $table = array( + 'table_type' => 'BASE TABLE', + 'view_definition' => null, // VIEW only + 'check_option' => null, // VIEW only + 'is_updatable' => null, // VIEW only + ); + } else { + $table = array( + 'table_type' => 'VIEW', + 'view_definition' => null, + 'check_option' => 'NONE', + 'is_updatable' => false, + ); + + // Parse out extra data + if (null !== ($data = $this->parseView($row['sql']))) { + $table = array_merge($table, $data); + } + } + $tables[$row['name']] = $table; + } + $this->data['table_names'][$schema] = $tables; + } + + protected function loadColumnData($table, $schema) + { + if (isset($this->data['columns'][$schema][$table])) { + return; + } + $this->prepareDataHierarchy('columns', $schema, $table); + $this->prepareDataHierarchy('sqlite_columns', $schema, $table); + + $p = $this->adapter->getPlatform(); + + + $results = $this->fetchPragma('table_info', $table, $schema); + + $columns = array(); + + foreach ($results as $row) { + $columns[$row['name']] = array( + // cid appears to be zero-based, ordinal position needs to be one-based + 'ordinal_position' => $row['cid'] + 1, + 'column_default' => $row['dflt_value'], + 'is_nullable' => !((bool) $row['notnull']), + 'data_type' => $row['type'], + 'character_maximum_length' => null, + 'character_octet_length' => null, + 'numeric_precision' => null, + 'numeric_scale' => null, + 'numeric_unsigned' => null, + 'erratas' => array(), + ); + // TODO: populate character_ and numeric_values with correct info + } + + $this->data['columns'][$schema][$table] = $columns; + $this->data['sqlite_columns'][$schema][$table] = $results; + } + + protected function loadConstraintData($table, $schema) + { + if (isset($this->data['constraints'][$schema][$table])) { + return; + } + + $this->prepareDataHierarchy('constraints', $schema, $table); + + $this->loadColumnData($table, $schema); + $primaryKey = array(); + + foreach ($this->data['sqlite_columns'][$schema][$table] as $col) { + if ((bool) $col['pk']) { + $primaryKey[] = $col['name']; + } + } + + if (empty($primaryKey)) { + $primaryKey = null; + } + $constraints = array(); + $indexes = $this->fetchPragma('index_list', $table, $schema); + foreach ($indexes as $index) { + if (!((bool) $index['unique'])) { + continue; + } + $constraint = array( + 'constraint_name' => $index['name'], + 'constraint_type' => 'UNIQUE', + 'table_name' => $table, + 'columns' => array(), + ); + + $info = $this->fetchPragma('index_info', $index['name'], $schema); + + foreach ($info as $column) { + $constraint['columns'][] = $column['name']; + } + if ($primaryKey === $constraint['columns']) { + $constraint['constraint_type'] = 'PRIMARY KEY'; + $primaryKey = null; + } + $constraints[$constraint['constraint_name']] = $constraint; + } + + if (null !== $primaryKey) { + $constraintName = '_zf_' . $table . '_PRIMARY'; + $constraints[$constraintName] = array( + 'constraint_name' => $constraintName, + 'constraint_type' => 'PRIMARY KEY', + 'table_name' => $table, + 'columns' => $primaryKey, + ); + } + + $foreignKeys = $this->fetchPragma('foreign_key_list', $table, $schema); + + $id = $name = null; + foreach ($foreignKeys as $fk) { + if ($id !== $fk['id']) { + $id = $fk['id']; + $name = '_zf_' . $table . '_FOREIGN_KEY_' . ($id + 1); + $constraints[$name] = array( + 'constraint_name' => $name, + 'constraint_type' => 'FOREIGN KEY', + 'table_name' => $table, + 'columns' => array(), + 'referenced_table_schema' => $schema, + 'referenced_table_name' => $fk['table'], + 'referenced_columns' => array(), + // TODO: Verify match, on_update, and on_delete values conform to SQL Standard + 'match_option' => strtoupper($fk['match']), + 'update_rule' => strtoupper($fk['on_update']), + 'delete_rule' => strtoupper($fk['on_delete']), + ); + } + $constraints[$name]['columns'][] = $fk['from']; + $constraints[$name]['referenced_columns'][] = $fk['to']; + } + + $this->data['constraints'][$schema][$table] = $constraints; + } + + protected function loadTriggerData($schema) + { + if (isset($this->data['triggers'][$schema])) { + return; + } + + $this->prepareDataHierarchy('triggers', $schema); + + $p = $this->adapter->getPlatform(); + + $sql = 'SELECT "name", "tbl_name", "sql" FROM ' + . $p->quoteIdentifierChain(array($schema, 'sqlite_master')) + . ' WHERE "type" = \'trigger\''; + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + $triggers = array(); + foreach ($results->toArray() as $row) { + $trigger = array( + 'trigger_name' => $row['name'], + 'event_manipulation' => null, // in $row['sql'] + 'event_object_catalog' => null, + 'event_object_schema' => $schema, + 'event_object_table' => $row['tbl_name'], + 'action_order' => 0, + 'action_condition' => null, // in $row['sql'] + 'action_statement' => null, // in $row['sql'] + 'action_orientation' => 'ROW', + 'action_timing' => null, // in $row['sql'] + 'action_reference_old_table' => null, + 'action_reference_new_table' => null, + 'action_reference_old_row' => 'OLD', + 'action_reference_new_row' => 'NEW', + 'created' => null, + ); + + // Parse out extra data + if (null !== ($data = $this->parseTrigger($row['sql']))) { + $trigger = array_merge($trigger, $data); + } + $triggers[$trigger['trigger_name']] = $trigger; + } + + $this->data['triggers'][$schema] = $triggers; + } + + protected function fetchPragma($name, $value = null, $schema = null) + { + $p = $this->adapter->getPlatform(); + + $sql = 'PRAGMA '; + + if (null !== $schema) { + $sql .= $p->quoteIdentifier($schema) . '.'; + } + $sql .= $name; + + if (null !== $value) { + $sql .= '(' . $p->quoteTrustedValue($value) . ')'; + } + + $results = $this->adapter->query($sql, Adapter::QUERY_MODE_EXECUTE); + if ($results instanceof ResultSetInterface) { + return $results->toArray(); + } + return array(); + } + + protected function parseView($sql) + { + static $re = null; + if (null === $re) { + $identifierChain = $this->getIdentifierChainRegularExpression(); + $re = $this->buildRegularExpression(array( + 'CREATE', + array('TEMP|TEMPORARY'), + 'VIEW', + array('IF', 'NOT', 'EXISTS'), + $identifierChain, + 'AS', + '(?.+)', + array(';'), + )); + } + + if (!preg_match($re, $sql, $matches)) { + return null; + } + return array( + 'view_definition' => $matches['view_definition'], + ); + } + + protected function parseTrigger($sql) + { + static $re = null; + if (null === $re) { + $identifier = $this->getIdentifierRegularExpression(); + $identifierList = $this->getIdentifierListRegularExpression(); + $identifierChain = $this->getIdentifierChainRegularExpression(); + $re = $this->buildRegularExpression(array( + 'CREATE', + array('TEMP|TEMPORARY'), + 'TRIGGER', + array('IF', 'NOT', 'EXISTS'), + $identifierChain, + array('(?BEFORE|AFTER|INSTEAD\\s+OF)',), + '(?DELETE|INSERT|UPDATE)', + array('OF', '(?' . $identifierList . ')'), + 'ON', + '(?' . $identifier . ')', + array('FOR', 'EACH', 'ROW'), + array('WHEN', '(?.+)'), + '(?BEGIN', + '.+', + 'END)', + array(';'), + )); + } + + if (!preg_match($re, $sql, $matches)) { + return null; + } + $data = array(); + + foreach ($matches as $key => $value) { + if (is_string($key)) { + $data[$key] = $value; + } + } + + // Normalize data and populate defaults, if necessary + + $data['event_manipulation'] = strtoupper($data['event_manipulation']); + if (empty($data['action_condition'])) { + $data['action_condition'] = null; + } + if (!empty($data['action_timing'])) { + $data['action_timing'] = strtoupper($data['action_timing']); + if ('I' == $data['action_timing'][0]) { + // normalize the white-space between the two words + $data['action_timing'] = 'INSTEAD OF'; + } + } else { + $data['action_timing'] = 'AFTER'; + } + unset($data['column_usage']); + + return $data; + } + + protected function buildRegularExpression(array $re) + { + foreach ($re as &$value) { + if (is_array($value)) { + $value = '(?:' . implode('\\s*+', $value) . '\\s*+)?'; + } else { + $value .= '\\s*+'; + } + } + unset($value); + $re = '/^' . implode('\\s*+', $re) . '$/'; + return $re; + } + + protected function getIdentifierRegularExpression() + { + static $re = null; + if (null === $re) { + $re = '(?:' . implode('|', array( + '"(?:[^"\\\\]++|\\\\.)*+"', + '`(?:[^`]++|``)*+`', + '\\[[^\\]]+\\]', + '[^\\s\\.]+', + )) . ')'; + } + + return $re; + } + + protected function getIdentifierChainRegularExpression() + { + static $re = null; + if (null === $re) { + $identifier = $this->getIdentifierRegularExpression(); + $re = $identifier . '(?:\\s*\\.\\s*' . $identifier . ')*+'; + } + return $re; + } + + protected function getIdentifierListRegularExpression() + { + static $re = null; + if (null === $re) { + $identifier = $this->getIdentifierRegularExpression(); + $re = $identifier . '(?:\\s*,\\s*' . $identifier . ')*+'; + } + return $re; + } +} diff --git a/library/Zend/Db/README.md b/library/Zend/Db/README.md new file mode 100755 index 0000000000..5c67884071 --- /dev/null +++ b/library/Zend/Db/README.md @@ -0,0 +1,15 @@ +DB Component from ZF2 +===================== + +This is the DB component for ZF2. + +- File issues at https://github.com/zendframework/zf2/issues +- Create pull requests against https://github.com/zendframework/zf2 +- Documentation is at http://framework.zend.com/docs + +LICENSE +------- + +The files in this archive are released under the [Zend Framework +license](http://framework.zend.com/license), which is a 3-clause BSD license. + diff --git a/library/Zend/Db/ResultSet/AbstractResultSet.php b/library/Zend/Db/ResultSet/AbstractResultSet.php new file mode 100755 index 0000000000..0db4c2d385 --- /dev/null +++ b/library/Zend/Db/ResultSet/AbstractResultSet.php @@ -0,0 +1,280 @@ +buffer)) { + $this->buffer = array(); + } + + if ($dataSource instanceof ResultInterface) { + $this->count = $dataSource->count(); + $this->fieldCount = $dataSource->getFieldCount(); + $this->dataSource = $dataSource; + if ($dataSource->isBuffered()) { + $this->buffer = -1; + } + if (is_array($this->buffer)) { + $this->dataSource->rewind(); + } + return $this; + } + + if (is_array($dataSource)) { + // its safe to get numbers from an array + $first = current($dataSource); + reset($dataSource); + $this->count = count($dataSource); + $this->fieldCount = count($first); + $this->dataSource = new ArrayIterator($dataSource); + $this->buffer = -1; // array's are a natural buffer + } elseif ($dataSource instanceof IteratorAggregate) { + $this->dataSource = $dataSource->getIterator(); + } elseif ($dataSource instanceof Iterator) { + $this->dataSource = $dataSource; + } else { + throw new Exception\InvalidArgumentException('DataSource provided is not an array, nor does it implement Iterator or IteratorAggregate'); + } + + if ($this->count == null && $this->dataSource instanceof Countable) { + $this->count = $this->dataSource->count(); + } + + return $this; + } + + public function buffer() + { + if ($this->buffer === -2) { + throw new Exception\RuntimeException('Buffering must be enabled before iteration is started'); + } elseif ($this->buffer === null) { + $this->buffer = array(); + if ($this->dataSource instanceof ResultInterface) { + $this->dataSource->rewind(); + } + } + return $this; + } + + public function isBuffered() + { + if ($this->buffer === -1 || is_array($this->buffer)) { + return true; + } + return false; + } + + /** + * Get the data source used to create the result set + * + * @return null|Iterator + */ + public function getDataSource() + { + return $this->dataSource; + } + + /** + * Retrieve count of fields in individual rows of the result set + * + * @return int + */ + public function getFieldCount() + { + if (null !== $this->fieldCount) { + return $this->fieldCount; + } + + $dataSource = $this->getDataSource(); + if (null === $dataSource) { + return 0; + } + + $dataSource->rewind(); + if (!$dataSource->valid()) { + $this->fieldCount = 0; + return 0; + } + + $row = $dataSource->current(); + if (is_object($row) && $row instanceof Countable) { + $this->fieldCount = $row->count(); + return $this->fieldCount; + } + + $row = (array) $row; + $this->fieldCount = count($row); + return $this->fieldCount; + } + + /** + * Iterator: move pointer to next item + * + * @return void + */ + public function next() + { + if ($this->buffer === null) { + $this->buffer = -2; // implicitly disable buffering from here on + } + $this->dataSource->next(); + $this->position++; + } + + /** + * Iterator: retrieve current key + * + * @return mixed + */ + public function key() + { + return $this->position; + } + + /** + * Iterator: get current item + * + * @return array + */ + public function current() + { + if ($this->buffer === null) { + $this->buffer = -2; // implicitly disable buffering from here on + } elseif (is_array($this->buffer) && isset($this->buffer[$this->position])) { + return $this->buffer[$this->position]; + } + $data = $this->dataSource->current(); + if (is_array($this->buffer)) { + $this->buffer[$this->position] = $data; + } + return $data; + } + + /** + * Iterator: is pointer valid? + * + * @return bool + */ + public function valid() + { + if (is_array($this->buffer) && isset($this->buffer[$this->position])) { + return true; + } + if ($this->dataSource instanceof Iterator) { + return $this->dataSource->valid(); + } else { + $key = key($this->dataSource); + return ($key !== null); + } + } + + /** + * Iterator: rewind + * + * @return void + */ + public function rewind() + { + if (!is_array($this->buffer)) { + if ($this->dataSource instanceof Iterator) { + $this->dataSource->rewind(); + } else { + reset($this->dataSource); + } + } + $this->position = 0; + } + + /** + * Countable: return count of rows + * + * @return int + */ + public function count() + { + if ($this->count !== null) { + return $this->count; + } + $this->count = count($this->dataSource); + return $this->count; + } + + /** + * Cast result set to array of arrays + * + * @return array + * @throws Exception\RuntimeException if any row is not castable to an array + */ + public function toArray() + { + $return = array(); + foreach ($this as $row) { + if (is_array($row)) { + $return[] = $row; + } elseif (method_exists($row, 'toArray')) { + $return[] = $row->toArray(); + } elseif (method_exists($row, 'getArrayCopy')) { + $return[] = $row->getArrayCopy(); + } else { + throw new Exception\RuntimeException( + 'Rows as part of this DataSource, with type ' . gettype($row) . ' cannot be cast to an array' + ); + } + } + return $return; + } +} diff --git a/library/Zend/Db/ResultSet/Exception/ExceptionInterface.php b/library/Zend/Db/ResultSet/Exception/ExceptionInterface.php new file mode 100755 index 0000000000..7f7648b33f --- /dev/null +++ b/library/Zend/Db/ResultSet/Exception/ExceptionInterface.php @@ -0,0 +1,16 @@ +setHydrator(($hydrator) ?: new ArraySerializable); + $this->setObjectPrototype(($objectPrototype) ?: new ArrayObject); + } + + /** + * Set the row object prototype + * + * @param object $objectPrototype + * @throws Exception\InvalidArgumentException + * @return ResultSet + */ + public function setObjectPrototype($objectPrototype) + { + if (!is_object($objectPrototype)) { + throw new Exception\InvalidArgumentException( + 'An object must be set as the object prototype, a ' . gettype($objectPrototype) . ' was provided.' + ); + } + $this->objectPrototype = $objectPrototype; + return $this; + } + + /** + * Set the hydrator to use for each row object + * + * @param HydratorInterface $hydrator + * @return HydratingResultSet + */ + public function setHydrator(HydratorInterface $hydrator) + { + $this->hydrator = $hydrator; + return $this; + } + + /** + * Get the hydrator to use for each row object + * + * @return HydratorInterface + */ + public function getHydrator() + { + return $this->hydrator; + } + + /** + * Iterator: get current item + * + * @return object + */ + public function current() + { + if ($this->buffer === null) { + $this->buffer = -2; // implicitly disable buffering from here on + } elseif (is_array($this->buffer) && isset($this->buffer[$this->position])) { + return $this->buffer[$this->position]; + } + $data = $this->dataSource->current(); + $object = is_array($data) ? $this->hydrator->hydrate($data, clone $this->objectPrototype) : false; + + if (is_array($this->buffer)) { + $this->buffer[$this->position] = $object; + } + + return $object; + } + + /** + * Cast result set to array of arrays + * + * @return array + * @throws Exception\RuntimeException if any row is not castable to an array + */ + public function toArray() + { + $return = array(); + foreach ($this as $row) { + $return[] = $this->getHydrator()->extract($row); + } + return $return; + } +} diff --git a/library/Zend/Db/ResultSet/ResultSet.php b/library/Zend/Db/ResultSet/ResultSet.php new file mode 100755 index 0000000000..2286410c65 --- /dev/null +++ b/library/Zend/Db/ResultSet/ResultSet.php @@ -0,0 +1,112 @@ +returnType = (in_array($returnType, array(self::TYPE_ARRAY, self::TYPE_ARRAYOBJECT))) ? $returnType : self::TYPE_ARRAYOBJECT; + if ($this->returnType === self::TYPE_ARRAYOBJECT) { + $this->setArrayObjectPrototype(($arrayObjectPrototype) ?: new ArrayObject(array(), ArrayObject::ARRAY_AS_PROPS)); + } + } + + /** + * Set the row object prototype + * + * @param ArrayObject $arrayObjectPrototype + * @throws Exception\InvalidArgumentException + * @return ResultSet + */ + public function setArrayObjectPrototype($arrayObjectPrototype) + { + if (!is_object($arrayObjectPrototype) + || (!$arrayObjectPrototype instanceof ArrayObject && !method_exists($arrayObjectPrototype, 'exchangeArray')) + + ) { + throw new Exception\InvalidArgumentException('Object must be of type ArrayObject, or at least implement exchangeArray'); + } + $this->arrayObjectPrototype = $arrayObjectPrototype; + return $this; + } + + /** + * Get the row object prototype + * + * @return ArrayObject + */ + public function getArrayObjectPrototype() + { + return $this->arrayObjectPrototype; + } + + /** + * Get the return type to use when returning objects from the set + * + * @return string + */ + public function getReturnType() + { + return $this->returnType; + } + + /** + * @return array|\ArrayObject|null + */ + public function current() + { + $data = parent::current(); + + if ($this->returnType === self::TYPE_ARRAYOBJECT && is_array($data)) { + /** @var $ao ArrayObject */ + $ao = clone $this->arrayObjectPrototype; + if ($ao instanceof ArrayObject || method_exists($ao, 'exchangeArray')) { + $ao->exchangeArray($data); + } + return $ao; + } + + return $data; + } +} diff --git a/library/Zend/Db/ResultSet/ResultSetInterface.php b/library/Zend/Db/ResultSet/ResultSetInterface.php new file mode 100755 index 0000000000..c2bbd73b27 --- /dev/null +++ b/library/Zend/Db/ResultSet/ResultSetInterface.php @@ -0,0 +1,33 @@ +isInitialized) { + return; + } + + if (!$this->featureSet instanceof Feature\FeatureSet) { + $this->featureSet = new Feature\FeatureSet; + } + + $this->featureSet->setRowGateway($this); + $this->featureSet->apply('preInitialize', array()); + + if (!is_string($this->table) && !$this->table instanceof TableIdentifier) { + throw new Exception\RuntimeException('This row object does not have a valid table set.'); + } + + if ($this->primaryKeyColumn == null) { + throw new Exception\RuntimeException('This row object does not have a primary key column set.'); + } elseif (is_string($this->primaryKeyColumn)) { + $this->primaryKeyColumn = (array) $this->primaryKeyColumn; + } + + if (!$this->sql instanceof Sql) { + throw new Exception\RuntimeException('This row object does not have a Sql object set.'); + } + + $this->featureSet->apply('postInitialize', array()); + + $this->isInitialized = true; + } + + /** + * Populate Data + * + * @param array $rowData + * @param bool $rowExistsInDatabase + * @return AbstractRowGateway + */ + public function populate(array $rowData, $rowExistsInDatabase = false) + { + $this->initialize(); + + $this->data = $rowData; + if ($rowExistsInDatabase == true) { + $this->processPrimaryKeyData(); + } else { + $this->primaryKeyData = null; + } + + return $this; + } + + /** + * @param mixed $array + * @return array|void + */ + public function exchangeArray($array) + { + return $this->populate($array, true); + } + + /** + * Save + * + * @return int + */ + public function save() + { + $this->initialize(); + + if ($this->rowExistsInDatabase()) { + // UPDATE + + $data = $this->data; + $where = array(); + $isPkModified = false; + + // primary key is always an array even if its a single column + foreach ($this->primaryKeyColumn as $pkColumn) { + $where[$pkColumn] = $this->primaryKeyData[$pkColumn]; + if ($data[$pkColumn] == $this->primaryKeyData[$pkColumn]) { + unset($data[$pkColumn]); + } else { + $isPkModified = true; + } + } + + $statement = $this->sql->prepareStatementForSqlObject($this->sql->update()->set($data)->where($where)); + $result = $statement->execute(); + $rowsAffected = $result->getAffectedRows(); + unset($statement, $result); // cleanup + + // If one or more primary keys are modified, we update the where clause + if ($isPkModified) { + foreach ($this->primaryKeyColumn as $pkColumn) { + if ($data[$pkColumn] != $this->primaryKeyData[$pkColumn]) { + $where[$pkColumn] = $data[$pkColumn]; + } + } + } + } else { + // INSERT + $insert = $this->sql->insert(); + $insert->values($this->data); + + $statement = $this->sql->prepareStatementForSqlObject($insert); + + $result = $statement->execute(); + if (($primaryKeyValue = $result->getGeneratedValue()) && count($this->primaryKeyColumn) == 1) { + $this->primaryKeyData = array($this->primaryKeyColumn[0] => $primaryKeyValue); + } else { + // make primary key data available so that $where can be complete + $this->processPrimaryKeyData(); + } + $rowsAffected = $result->getAffectedRows(); + unset($statement, $result); // cleanup + + $where = array(); + // primary key is always an array even if its a single column + foreach ($this->primaryKeyColumn as $pkColumn) { + $where[$pkColumn] = $this->primaryKeyData[$pkColumn]; + } + } + + // refresh data + $statement = $this->sql->prepareStatementForSqlObject($this->sql->select()->where($where)); + $result = $statement->execute(); + $rowData = $result->current(); + unset($statement, $result); // cleanup + + // make sure data and original data are in sync after save + $this->populate($rowData, true); + + // return rows affected + return $rowsAffected; + } + + /** + * Delete + * + * @return int + */ + public function delete() + { + $this->initialize(); + + $where = array(); + // primary key is always an array even if its a single column + foreach ($this->primaryKeyColumn as $pkColumn) { + $where[$pkColumn] = $this->primaryKeyData[$pkColumn]; + } + + // @todo determine if we need to do a select to ensure 1 row will be affected + + $statement = $this->sql->prepareStatementForSqlObject($this->sql->delete()->where($where)); + $result = $statement->execute(); + + $affectedRows = $result->getAffectedRows(); + if ($affectedRows == 1) { + // detach from database + $this->primaryKeyData = null; + } + + return $affectedRows; + } + + /** + * Offset Exists + * + * @param string $offset + * @return bool + */ + public function offsetExists($offset) + { + return array_key_exists($offset, $this->data); + } + + /** + * Offset get + * + * @param string $offset + * @return mixed + */ + public function offsetGet($offset) + { + return $this->data[$offset]; + } + + /** + * Offset set + * + * @param string $offset + * @param mixed $value + * @return RowGateway + */ + public function offsetSet($offset, $value) + { + $this->data[$offset] = $value; + return $this; + } + + /** + * Offset unset + * + * @param string $offset + * @return AbstractRowGateway + */ + public function offsetUnset($offset) + { + $this->data[$offset] = null; + return $this; + } + + /** + * @return int + */ + public function count() + { + return count($this->data); + } + + /** + * To array + * + * @return array + */ + public function toArray() + { + return $this->data; + } + + /** + * __get + * + * @param string $name + * @throws Exception\InvalidArgumentException + * @return mixed + */ + public function __get($name) + { + if (array_key_exists($name, $this->data)) { + return $this->data[$name]; + } else { + throw new Exception\InvalidArgumentException('Not a valid column in this row: ' . $name); + } + } + + /** + * __set + * + * @param string $name + * @param mixed $value + * @return void + */ + public function __set($name, $value) + { + $this->offsetSet($name, $value); + } + + /** + * __isset + * + * @param string $name + * @return bool + */ + public function __isset($name) + { + return $this->offsetExists($name); + } + + /** + * __unset + * + * @param string $name + * @return void + */ + public function __unset($name) + { + $this->offsetUnset($name); + } + + /** + * @return bool + */ + public function rowExistsInDatabase() + { + return ($this->primaryKeyData !== null); + } + + /** + * @throws Exception\RuntimeException + */ + protected function processPrimaryKeyData() + { + $this->primaryKeyData = array(); + foreach ($this->primaryKeyColumn as $column) { + if (!isset($this->data[$column])) { + throw new Exception\RuntimeException('While processing primary key data, a known key ' . $column . ' was not found in the data array'); + } + $this->primaryKeyData[$column] = $this->data[$column]; + } + } +} diff --git a/library/Zend/Db/RowGateway/Exception/ExceptionInterface.php b/library/Zend/Db/RowGateway/Exception/ExceptionInterface.php new file mode 100755 index 0000000000..7bb37fc982 --- /dev/null +++ b/library/Zend/Db/RowGateway/Exception/ExceptionInterface.php @@ -0,0 +1,16 @@ +rowGateway = $rowGateway; + } + + /** + * @throws \Zend\Db\RowGateway\Exception\RuntimeException + */ + public function initialize() + { + throw new Exception\RuntimeException('This method is not intended to be called on this object.'); + } + + /** + * @return array + */ + public function getMagicMethodSpecifications() + { + return array(); + } +} diff --git a/library/Zend/Db/RowGateway/Feature/FeatureSet.php b/library/Zend/Db/RowGateway/Feature/FeatureSet.php new file mode 100755 index 0000000000..de3b2344fb --- /dev/null +++ b/library/Zend/Db/RowGateway/Feature/FeatureSet.php @@ -0,0 +1,149 @@ +addFeatures($features); + } + } + + public function setRowGateway(AbstractRowGateway $rowGateway) + { + $this->rowGateway = $rowGateway; + foreach ($this->features as $feature) { + $feature->setRowGateway($this->rowGateway); + } + return $this; + } + + public function getFeatureByClassName($featureClassName) + { + $feature = false; + foreach ($this->features as $potentialFeature) { + if ($potentialFeature instanceof $featureClassName) { + $feature = $potentialFeature; + break; + } + } + return $feature; + } + + public function addFeatures(array $features) + { + foreach ($features as $feature) { + $this->addFeature($feature); + } + return $this; + } + + public function addFeature(AbstractFeature $feature) + { + $this->features[] = $feature; + $feature->setRowGateway($feature); + return $this; + } + + public function apply($method, $args) + { + foreach ($this->features as $feature) { + if (method_exists($feature, $method)) { + $return = call_user_func_array(array($feature, $method), $args); + if ($return === self::APPLY_HALT) { + break; + } + } + } + } + + /** + * @param string $property + * @return bool + */ + public function canCallMagicGet($property) + { + return false; + } + + /** + * @param string $property + * @return mixed + */ + public function callMagicGet($property) + { + $return = null; + return $return; + } + + /** + * @param string $property + * @return bool + */ + public function canCallMagicSet($property) + { + return false; + } + + /** + * @param $property + * @param $value + * @return mixed + */ + public function callMagicSet($property, $value) + { + $return = null; + return $return; + } + + /** + * @param string $method + * @return bool + */ + public function canCallMagicCall($method) + { + return false; + } + + /** + * @param string $method + * @param array $arguments + * @return mixed + */ + public function callMagicCall($method, $arguments) + { + $return = null; + return $return; + } +} diff --git a/library/Zend/Db/RowGateway/RowGateway.php b/library/Zend/Db/RowGateway/RowGateway.php new file mode 100755 index 0000000000..df2295c14d --- /dev/null +++ b/library/Zend/Db/RowGateway/RowGateway.php @@ -0,0 +1,48 @@ +primaryKeyColumn = empty($primaryKeyColumn) ? null : (array) $primaryKeyColumn; + + // set table + $this->table = $table; + + // set Sql object + if ($adapterOrSql instanceof Sql) { + $this->sql = $adapterOrSql; + } elseif ($adapterOrSql instanceof Adapter) { + $this->sql = new Sql($adapterOrSql, $this->table); + } else { + throw new Exception\InvalidArgumentException('A valid Sql object was not provided.'); + } + + if ($this->sql->getTable() !== $this->table) { + throw new Exception\InvalidArgumentException('The Sql object provided does not have a table that matches this row object'); + } + + $this->initialize(); + } +} diff --git a/library/Zend/Db/RowGateway/RowGatewayInterface.php b/library/Zend/Db/RowGateway/RowGatewayInterface.php new file mode 100755 index 0000000000..e0a20b554d --- /dev/null +++ b/library/Zend/Db/RowGateway/RowGatewayInterface.php @@ -0,0 +1,16 @@ + '', 'subselectCount' => 0); + + /** + * @var array + */ + protected $instanceParameterIndex = array(); + + protected function processExpression(ExpressionInterface $expression, PlatformInterface $platform, DriverInterface $driver = null, $namedParameterPrefix = null) + { + // static counter for the number of times this method was invoked across the PHP runtime + static $runtimeExpressionPrefix = 0; + + if ($driver && ((!is_string($namedParameterPrefix) || $namedParameterPrefix == ''))) { + $namedParameterPrefix = sprintf('expr%04dParam', ++$runtimeExpressionPrefix); + } + + $sql = ''; + $statementContainer = new StatementContainer; + $parameterContainer = $statementContainer->getParameterContainer(); + + // initialize variables + $parts = $expression->getExpressionData(); + + if (!isset($this->instanceParameterIndex[$namedParameterPrefix])) { + $this->instanceParameterIndex[$namedParameterPrefix] = 1; + } + + $expressionParamIndex = &$this->instanceParameterIndex[$namedParameterPrefix]; + + foreach ($parts as $part) { + // if it is a string, simply tack it onto the return sql "specification" string + if (is_string($part)) { + $sql .= $part; + continue; + } + + if (!is_array($part)) { + throw new Exception\RuntimeException('Elements returned from getExpressionData() array must be a string or array.'); + } + + // process values and types (the middle and last position of the expression data) + $values = $part[1]; + $types = (isset($part[2])) ? $part[2] : array(); + foreach ($values as $vIndex => $value) { + if (isset($types[$vIndex]) && $types[$vIndex] == ExpressionInterface::TYPE_IDENTIFIER) { + $values[$vIndex] = $platform->quoteIdentifierInFragment($value); + } elseif (isset($types[$vIndex]) && $types[$vIndex] == ExpressionInterface::TYPE_VALUE && $value instanceof Select) { + // process sub-select + if ($driver) { + $values[$vIndex] = '(' . $this->processSubSelect($value, $platform, $driver, $parameterContainer) . ')'; + } else { + $values[$vIndex] = '(' . $this->processSubSelect($value, $platform) . ')'; + } + } elseif (isset($types[$vIndex]) && $types[$vIndex] == ExpressionInterface::TYPE_VALUE && $value instanceof ExpressionInterface) { + // recursive call to satisfy nested expressions + $innerStatementContainer = $this->processExpression($value, $platform, $driver, $namedParameterPrefix . $vIndex . 'subpart'); + $values[$vIndex] = $innerStatementContainer->getSql(); + if ($driver) { + $parameterContainer->merge($innerStatementContainer->getParameterContainer()); + } + } elseif (isset($types[$vIndex]) && $types[$vIndex] == ExpressionInterface::TYPE_VALUE) { + // if prepareType is set, it means that this particular value must be + // passed back to the statement in a way it can be used as a placeholder value + if ($driver) { + $name = $namedParameterPrefix . $expressionParamIndex++; + $parameterContainer->offsetSet($name, $value); + $values[$vIndex] = $driver->formatParameterName($name); + continue; + } + + // if not a preparable statement, simply quote the value and move on + $values[$vIndex] = $platform->quoteValue($value); + } elseif (isset($types[$vIndex]) && $types[$vIndex] == ExpressionInterface::TYPE_LITERAL) { + $values[$vIndex] = $value; + } + } + + // after looping the values, interpolate them into the sql string (they might be placeholder names, or values) + $sql .= vsprintf($part[0], $values); + } + + $statementContainer->setSql($sql); + return $statementContainer; + } + + /** + * @param $specifications + * @param $parameters + * @return string + * @throws Exception\RuntimeException + */ + protected function createSqlFromSpecificationAndParameters($specifications, $parameters) + { + if (is_string($specifications)) { + return vsprintf($specifications, $parameters); + } + + $parametersCount = count($parameters); + foreach ($specifications as $specificationString => $paramSpecs) { + if ($parametersCount == count($paramSpecs)) { + break; + } + unset($specificationString, $paramSpecs); + } + + if (!isset($specificationString)) { + throw new Exception\RuntimeException( + 'A number of parameters was found that is not supported by this specification' + ); + } + + $topParameters = array(); + foreach ($parameters as $position => $paramsForPosition) { + if (isset($paramSpecs[$position]['combinedby'])) { + $multiParamValues = array(); + foreach ($paramsForPosition as $multiParamsForPosition) { + $ppCount = count($multiParamsForPosition); + if (!isset($paramSpecs[$position][$ppCount])) { + throw new Exception\RuntimeException('A number of parameters (' . $ppCount . ') was found that is not supported by this specification'); + } + $multiParamValues[] = vsprintf($paramSpecs[$position][$ppCount], $multiParamsForPosition); + } + $topParameters[] = implode($paramSpecs[$position]['combinedby'], $multiParamValues); + } elseif ($paramSpecs[$position] !== null) { + $ppCount = count($paramsForPosition); + if (!isset($paramSpecs[$position][$ppCount])) { + throw new Exception\RuntimeException('A number of parameters (' . $ppCount . ') was found that is not supported by this specification'); + } + $topParameters[] = vsprintf($paramSpecs[$position][$ppCount], $paramsForPosition); + } else { + $topParameters[] = $paramsForPosition; + } + } + return vsprintf($specificationString, $topParameters); + } + + protected function processSubSelect(Select $subselect, PlatformInterface $platform, DriverInterface $driver = null, ParameterContainer $parameterContainer = null) + { + if ($driver) { + $stmtContainer = new StatementContainer; + + // Track subselect prefix and count for parameters + $this->processInfo['subselectCount']++; + $subselect->processInfo['subselectCount'] = $this->processInfo['subselectCount']; + $subselect->processInfo['paramPrefix'] = 'subselect' . $subselect->processInfo['subselectCount']; + + // call subselect + if ($this instanceof PlatformDecoratorInterface) { + /** @var Select|PlatformDecoratorInterface $subselectDecorator */ + $subselectDecorator = clone $this; + $subselectDecorator->setSubject($subselect); + $subselectDecorator->prepareStatement(new Adapter($driver, $platform), $stmtContainer); + } else { + $subselect->prepareStatement(new Adapter($driver, $platform), $stmtContainer); + } + + // copy count + $this->processInfo['subselectCount'] = $subselect->processInfo['subselectCount']; + + $parameterContainer->merge($stmtContainer->getParameterContainer()->getNamedArray()); + $sql = $stmtContainer->getSql(); + } else { + if ($this instanceof PlatformDecoratorInterface) { + $subselectDecorator = clone $this; + $subselectDecorator->setSubject($subselect); + $sql = $subselectDecorator->getSqlString($platform); + } else { + $sql = $subselect->getSqlString($platform); + } + } + return $sql; + } +} diff --git a/library/Zend/Db/Sql/Ddl/AlterTable.php b/library/Zend/Db/Sql/Ddl/AlterTable.php new file mode 100755 index 0000000000..2721db5a23 --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/AlterTable.php @@ -0,0 +1,268 @@ + "ALTER TABLE %1\$s\n", + self::ADD_COLUMNS => array( + "%1\$s" => array( + array(1 => 'ADD COLUMN %1$s', 'combinedby' => ",\n") + ) + ), + self::CHANGE_COLUMNS => array( + "%1\$s" => array( + array(2 => 'CHANGE COLUMN %1$s %2$s', 'combinedby' => ",\n"), + ) + ), + self::DROP_COLUMNS => array( + "%1\$s" => array( + array(1 => 'DROP COLUMN %1$s', 'combinedby' => ",\n"), + ) + ), + self::ADD_CONSTRAINTS => array( + "%1\$s" => array( + array(1 => 'ADD %1$s', 'combinedby' => ",\n"), + ) + ), + self::DROP_CONSTRAINTS => array( + "%1\$s" => array( + array(1 => 'DROP CONSTRAINT %1$s', 'combinedby' => ",\n"), + ) + ) + ); + + /** + * @var string + */ + protected $table = ''; + + /** + * @param string $table + */ + public function __construct($table = '') + { + ($table) ? $this->setTable($table) : null; + } + + /** + * @param string $name + * @return self + */ + public function setTable($name) + { + $this->table = $name; + + return $this; + } + + /** + * @param Column\ColumnInterface $column + * @return self + */ + public function addColumn(Column\ColumnInterface $column) + { + $this->addColumns[] = $column; + + return $this; + } + + /** + * @param string $name + * @param Column\ColumnInterface $column + * @return self + */ + public function changeColumn($name, Column\ColumnInterface $column) + { + $this->changeColumns[$name] = $column; + + return $this; + } + + /** + * @param string $name + * @return self + */ + public function dropColumn($name) + { + $this->dropColumns[] = $name; + + return $this; + } + + /** + * @param string $name + * @return self + */ + public function dropConstraint($name) + { + $this->dropConstraints[] = $name; + + return $this; + } + + /** + * @param Constraint\ConstraintInterface $constraint + * @return self + */ + public function addConstraint(Constraint\ConstraintInterface $constraint) + { + $this->addConstraints[] = $constraint; + + return $this; + } + + /** + * @param string|null $key + * @return array + */ + public function getRawState($key = null) + { + $rawState = array( + self::TABLE => $this->table, + self::ADD_COLUMNS => $this->addColumns, + self::DROP_COLUMNS => $this->dropColumns, + self::CHANGE_COLUMNS => $this->changeColumns, + self::ADD_CONSTRAINTS => $this->addConstraints, + self::DROP_CONSTRAINTS => $this->dropConstraints, + ); + + return (isset($key) && array_key_exists($key, $rawState)) ? $rawState[$key] : $rawState; + } + + /** + * @param PlatformInterface $adapterPlatform + * @return string + */ + public function getSqlString(PlatformInterface $adapterPlatform = null) + { + // get platform, or create default + $adapterPlatform = ($adapterPlatform) ?: new AdapterSql92Platform; + + $sqls = array(); + $parameters = array(); + + foreach ($this->specifications as $name => $specification) { + $parameters[$name] = $this->{'process' . $name}($adapterPlatform, null, null, $sqls, $parameters); + if ($specification && is_array($parameters[$name]) && ($parameters[$name] != array(array()))) { + $sqls[$name] = $this->createSqlFromSpecificationAndParameters($specification, $parameters[$name]); + } + if (stripos($name, 'table') === false && $parameters[$name] !== array(array())) { + $sqls[] = ",\n"; + } + } + + // remove last ,\n + array_pop($sqls); + + $sql = implode('', $sqls); + + return $sql; + } + + protected function processTable(PlatformInterface $adapterPlatform = null) + { + return array($adapterPlatform->quoteIdentifier($this->table)); + } + + protected function processAddColumns(PlatformInterface $adapterPlatform = null) + { + $sqls = array(); + foreach ($this->addColumns as $column) { + $sqls[] = $this->processExpression($column, $adapterPlatform)->getSql(); + } + + return array($sqls); + } + + protected function processChangeColumns(PlatformInterface $adapterPlatform = null) + { + $sqls = array(); + foreach ($this->changeColumns as $name => $column) { + $sqls[] = array( + $adapterPlatform->quoteIdentifier($name), + $this->processExpression($column, $adapterPlatform)->getSql() + ); + } + + return array($sqls); + } + + protected function processDropColumns(PlatformInterface $adapterPlatform = null) + { + $sqls = array(); + foreach ($this->dropColumns as $column) { + $sqls[] = $adapterPlatform->quoteIdentifier($column); + } + + return array($sqls); + } + + protected function processAddConstraints(PlatformInterface $adapterPlatform = null) + { + $sqls = array(); + foreach ($this->addConstraints as $constraint) { + $sqls[] = $this->processExpression($constraint, $adapterPlatform); + } + + return array($sqls); + } + + protected function processDropConstraints(PlatformInterface $adapterPlatform = null) + { + $sqls = array(); + foreach ($this->dropConstraints as $constraint) { + $sqls[] = $adapterPlatform->quoteIdentifier($constraint); + } + + return array($sqls); + } +} diff --git a/library/Zend/Db/Sql/Ddl/Column/BigInteger.php b/library/Zend/Db/Sql/Ddl/Column/BigInteger.php new file mode 100755 index 0000000000..d915a948f3 --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/Column/BigInteger.php @@ -0,0 +1,18 @@ +setName($name); + $this->setLength($length); + $this->setNullable($nullable); + $this->setDefault($default); + $this->setOptions($options); + } + + /** + * @param int $length + * @return self + */ + public function setLength($length) + { + $this->length = $length; + return $this; + } + + /** + * @return int + */ + public function getLength() + { + return $this->length; + } + + /** + * @return array + */ + public function getExpressionData() + { + $spec = $this->specification; + + $params = array(); + $params[] = $this->name; + $params[] = $this->type; + + if ($this->length) { + $params[1] .= ' ' . $this->length; + } + + $types = array(self::TYPE_IDENTIFIER, self::TYPE_LITERAL); + + if (!$this->isNullable) { + $params[1] .= ' NOT NULL'; + } + + if ($this->default !== null) { + $spec .= ' DEFAULT %s'; + $params[] = $this->default; + $types[] = self::TYPE_VALUE; + } + + return array(array( + $spec, + $params, + $types, + )); + } +} diff --git a/library/Zend/Db/Sql/Ddl/Column/Boolean.php b/library/Zend/Db/Sql/Ddl/Column/Boolean.php new file mode 100755 index 0000000000..36c07187cb --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/Column/Boolean.php @@ -0,0 +1,42 @@ +name = $name; + } + + /** + * @return array + */ + public function getExpressionData() + { + $spec = $this->specification; + $params = array($this->name); + $types = array(self::TYPE_IDENTIFIER); + + return array(array( + $spec, + $params, + $types, + )); + } +} diff --git a/library/Zend/Db/Sql/Ddl/Column/Char.php b/library/Zend/Db/Sql/Ddl/Column/Char.php new file mode 100755 index 0000000000..507cfe2c60 --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/Column/Char.php @@ -0,0 +1,58 @@ +name = $name; + $this->length = $length; + } + + /** + * @return array + */ + public function getExpressionData() + { + $spec = $this->specification; + $params = array(); + + $types = array(self::TYPE_IDENTIFIER, self::TYPE_LITERAL); + $params[] = $this->name; + $params[] = $this->length; + + $types[] = self::TYPE_LITERAL; + $params[] = (!$this->isNullable) ? 'NOT NULL' : ''; + + $types[] = ($this->default !== null) ? self::TYPE_VALUE : self::TYPE_LITERAL; + $params[] = ($this->default !== null) ? $this->default : ''; + + return array(array( + $spec, + $params, + $types, + )); + } +} diff --git a/library/Zend/Db/Sql/Ddl/Column/Column.php b/library/Zend/Db/Sql/Ddl/Column/Column.php new file mode 100755 index 0000000000..de2f852b0d --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/Column/Column.php @@ -0,0 +1,164 @@ +setName($name); + } + + /** + * @param string $name + * @return self + */ + public function setName($name) + { + $this->name = $name; + return $this; + } + + /** + * @return null|string + */ + public function getName() + { + return $this->name; + } + + /** + * @param bool $nullable + * @return self + */ + public function setNullable($nullable) + { + $this->isNullable = (bool) $nullable; + return $this; + } + + /** + * @return bool + */ + public function isNullable() + { + return $this->isNullable; + } + + /** + * @param null|string|int $default + * @return self + */ + public function setDefault($default) + { + $this->default = $default; + return $this; + } + + /** + * @return null|string|int + */ + public function getDefault() + { + return $this->default; + } + + /** + * @param array $options + * @return self + */ + public function setOptions(array $options) + { + $this->options = $options; + return $this; + } + + /** + * @param string $name + * @param string $value + * @return self + */ + public function setOption($name, $value) + { + $this->options[$name] = $value; + return $this; + } + + /** + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * @return array + */ + public function getExpressionData() + { + $spec = $this->specification; + + $params = array(); + $params[] = $this->name; + $params[] = $this->type; + + $types = array(self::TYPE_IDENTIFIER, self::TYPE_LITERAL); + + if (!$this->isNullable) { + $params[1] .= ' NOT NULL'; + } + + if ($this->default !== null) { + $spec .= ' DEFAULT %s'; + $params[] = $this->default; + $types[] = self::TYPE_VALUE; + } + + return array(array( + $spec, + $params, + $types, + )); + } +} diff --git a/library/Zend/Db/Sql/Ddl/Column/ColumnInterface.php b/library/Zend/Db/Sql/Ddl/Column/ColumnInterface.php new file mode 100755 index 0000000000..331e5254f4 --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/Column/ColumnInterface.php @@ -0,0 +1,20 @@ +name = $name; + } + + /** + * @return array + */ + public function getExpressionData() + { + $spec = $this->specification; + $params = array(); + + $types = array(self::TYPE_IDENTIFIER); + $params[] = $this->name; + + $types[] = self::TYPE_LITERAL; + $params[] = (!$this->isNullable) ? 'NOT NULL' : ''; + + $types[] = ($this->default !== null) ? self::TYPE_VALUE : self::TYPE_LITERAL; + $params[] = ($this->default !== null) ? $this->default : ''; + + return array(array( + $spec, + $params, + $types, + )); + } +} diff --git a/library/Zend/Db/Sql/Ddl/Column/Decimal.php b/library/Zend/Db/Sql/Ddl/Column/Decimal.php new file mode 100755 index 0000000000..8a0ff25e3c --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/Column/Decimal.php @@ -0,0 +1,69 @@ +name = $name; + $this->precision = $precision; + $this->scale = $scale; + } + + /** + * @return array + */ + public function getExpressionData() + { + $spec = $this->specification; + $params = array(); + + $types = array(self::TYPE_IDENTIFIER, self::TYPE_LITERAL); + $params[] = $this->name; + $params[] = $this->precision; + + if ($this->scale !== null) { + $params[1] .= ', ' . $this->scale; + } + + $types[] = self::TYPE_LITERAL; + $params[] = (!$this->isNullable) ? 'NOT NULL' : ''; + + $types[] = ($this->default !== null) ? self::TYPE_VALUE : self::TYPE_LITERAL; + $params[] = ($this->default !== null) ? $this->default : ''; + + return array(array( + $spec, + $params, + $types, + )); + } +} diff --git a/library/Zend/Db/Sql/Ddl/Column/Float.php b/library/Zend/Db/Sql/Ddl/Column/Float.php new file mode 100755 index 0000000000..e866abcf55 --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/Column/Float.php @@ -0,0 +1,66 @@ +name = $name; + $this->digits = $digits; + $this->decimal = $decimal; + } + + /** + * @return array + */ + public function getExpressionData() + { + $spec = $this->specification; + $params = array(); + + $types = array(self::TYPE_IDENTIFIER, self::TYPE_LITERAL); + $params[] = $this->name; + $params[] = $this->digits; + $params[1] .= ', ' . $this->decimal; + + $types[] = self::TYPE_LITERAL; + $params[] = (!$this->isNullable) ? 'NOT NULL' : ''; + + $types[] = ($this->default !== null) ? self::TYPE_VALUE : self::TYPE_LITERAL; + $params[] = ($this->default !== null) ? $this->default : ''; + + return array(array( + $spec, + $params, + $types, + )); + } +} diff --git a/library/Zend/Db/Sql/Ddl/Column/Integer.php b/library/Zend/Db/Sql/Ddl/Column/Integer.php new file mode 100755 index 0000000000..5e424285c0 --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/Column/Integer.php @@ -0,0 +1,32 @@ +setName($name); + $this->setNullable($nullable); + $this->setDefault($default); + $this->setOptions($options); + } +} diff --git a/library/Zend/Db/Sql/Ddl/Column/Text.php b/library/Zend/Db/Sql/Ddl/Column/Text.php new file mode 100755 index 0000000000..3e40709093 --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/Column/Text.php @@ -0,0 +1,51 @@ +name = $name; + } + + /** + * @return array + */ + public function getExpressionData() + { + $spec = $this->specification; + $params = array(); + + $types = array(self::TYPE_IDENTIFIER, self::TYPE_LITERAL); + $params[] = $this->name; + + $types[] = self::TYPE_LITERAL; + $params[] = (!$this->isNullable) ? 'NOT NULL' : ''; + + $types[] = ($this->default !== null) ? self::TYPE_VALUE : self::TYPE_LITERAL; + $params[] = ($this->default !== null) ? $this->default : ''; + + return array(array( + $spec, + $params, + $types, + )); + } +} diff --git a/library/Zend/Db/Sql/Ddl/Column/Time.php b/library/Zend/Db/Sql/Ddl/Column/Time.php new file mode 100755 index 0000000000..68d3c66484 --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/Column/Time.php @@ -0,0 +1,50 @@ +name = $name; + } + + /** + * @return array + */ + public function getExpressionData() + { + $spec = $this->specification; + $params = array(); + + $types = array(self::TYPE_IDENTIFIER); + $params[] = $this->name; + + $types[] = self::TYPE_LITERAL; + $params[] = (!$this->isNullable) ? 'NOT NULL' : ''; + + $types[] = ($this->default !== null) ? self::TYPE_VALUE : self::TYPE_LITERAL; + $params[] = ($this->default !== null) ? $this->default : ''; + + return array(array( + $spec, + $params, + $types, + )); + } +} diff --git a/library/Zend/Db/Sql/Ddl/Column/Varchar.php b/library/Zend/Db/Sql/Ddl/Column/Varchar.php new file mode 100755 index 0000000000..49a718c78c --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/Column/Varchar.php @@ -0,0 +1,58 @@ +name = $name; + $this->length = $length; + } + + /** + * @return array + */ + public function getExpressionData() + { + $spec = $this->specification; + $params = array(); + + $types = array(self::TYPE_IDENTIFIER, self::TYPE_LITERAL); + $params[] = $this->name; + $params[] = $this->length; + + $types[] = self::TYPE_LITERAL; + $params[] = (!$this->isNullable) ? 'NOT NULL' : ''; + + $types[] = ($this->default !== null) ? self::TYPE_VALUE : self::TYPE_LITERAL; + $params[] = ($this->default !== null) ? $this->default : ''; + + return array(array( + $spec, + $params, + $types, + )); + } +} diff --git a/library/Zend/Db/Sql/Ddl/Constraint/AbstractConstraint.php b/library/Zend/Db/Sql/Ddl/Constraint/AbstractConstraint.php new file mode 100755 index 0000000000..19909fadb2 --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/Constraint/AbstractConstraint.php @@ -0,0 +1,58 @@ +setColumns($columns); + } + + /** + * @param null|string|array $columns + * @return self + */ + public function setColumns($columns) + { + if (!is_array($columns)) { + $columns = array($columns); + } + + $this->columns = $columns; + return $this; + } + + /** + * @param string $column + * @return self + */ + public function addColumn($column) + { + $this->columns[] = $column; + return $this; + } + + /** + * @return array + */ + public function getColumns() + { + return $this->columns; + } +} diff --git a/library/Zend/Db/Sql/Ddl/Constraint/Check.php b/library/Zend/Db/Sql/Ddl/Constraint/Check.php new file mode 100755 index 0000000000..1afbeb39cb --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/Constraint/Check.php @@ -0,0 +1,45 @@ +expression = $expression; + $this->name = $name; + } + + /** + * @return array + */ + public function getExpressionData() + { + return array(array( + $this->specification, + array($this->name, $this->expression), + array(self::TYPE_IDENTIFIER, self::TYPE_LITERAL), + )); + } +} diff --git a/library/Zend/Db/Sql/Ddl/Constraint/ConstraintInterface.php b/library/Zend/Db/Sql/Ddl/Constraint/ConstraintInterface.php new file mode 100755 index 0000000000..bcb9643943 --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/Constraint/ConstraintInterface.php @@ -0,0 +1,17 @@ +setName($name); + $this->setColumns($column); + $this->setReferenceTable($referenceTable); + $this->setReferenceColumn($referenceColumn); + (!$onDeleteRule) ?: $this->setOnDeleteRule($onDeleteRule); + (!$onUpdateRule) ?: $this->setOnUpdateRule($onUpdateRule); + } + + /** + * @param string $name + * @return self + */ + public function setName($name) + { + $this->name = $name; + return $this; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $referenceTable + * @return self + */ + public function setReferenceTable($referenceTable) + { + $this->referenceTable = $referenceTable; + return $this; + } + + /** + * @return string + */ + public function getReferenceTable() + { + return $this->referenceTable; + } + + /** + * @param string $referenceColumn + * @return self + */ + public function setReferenceColumn($referenceColumn) + { + $this->referenceColumn = $referenceColumn; + return $this; + } + + /** + * @return string + */ + public function getReferenceColumn() + { + return $this->referenceColumn; + } + + /** + * @param string $onDeleteRule + * @return self + */ + public function setOnDeleteRule($onDeleteRule) + { + $this->onDeleteRule = $onDeleteRule; + return $this; + } + + /** + * @return string + */ + public function getOnDeleteRule() + { + return $this->onDeleteRule; + } + + /** + * @param string $onUpdateRule + * @return self + */ + public function setOnUpdateRule($onUpdateRule) + { + $this->onUpdateRule = $onUpdateRule; + return $this; + } + + /** + * @return string + */ + public function getOnUpdateRule() + { + return $this->onUpdateRule; + } + + /** + * @return array + */ + public function getExpressionData() + { + return array(array( + $this->specification, + array( + $this->name, + $this->columns[0], + $this->referenceTable, + $this->referenceColumn, + $this->onDeleteRule, + $this->onUpdateRule, + ), + array( + self::TYPE_IDENTIFIER, + self::TYPE_IDENTIFIER, + self::TYPE_IDENTIFIER, + self::TYPE_IDENTIFIER, + self::TYPE_LITERAL, + self::TYPE_LITERAL, + ), + )); + } +} diff --git a/library/Zend/Db/Sql/Ddl/Constraint/PrimaryKey.php b/library/Zend/Db/Sql/Ddl/Constraint/PrimaryKey.php new file mode 100755 index 0000000000..84124a4d0a --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/Constraint/PrimaryKey.php @@ -0,0 +1,36 @@ +columns); + $newSpecParts = array_fill(0, $colCount, '%s'); + $newSpecTypes = array_fill(0, $colCount, self::TYPE_IDENTIFIER); + + $newSpec = sprintf($this->specification, implode(', ', $newSpecParts)); + + return array(array( + $newSpec, + $this->columns, + $newSpecTypes, + )); + } +} diff --git a/library/Zend/Db/Sql/Ddl/Constraint/UniqueKey.php b/library/Zend/Db/Sql/Ddl/Constraint/UniqueKey.php new file mode 100755 index 0000000000..8d871054e1 --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/Constraint/UniqueKey.php @@ -0,0 +1,55 @@ +setColumns($column); + $this->name = $name; + } + + /** + * @return array + */ + public function getExpressionData() + { + $colCount = count($this->columns); + + $values = array(); + $values[] = ($this->name) ? $this->name : ''; + + $newSpecTypes = array(self::TYPE_IDENTIFIER); + $newSpecParts = array(); + + for ($i = 0; $i < $colCount; $i++) { + $newSpecParts[] = '%s'; + $newSpecTypes[] = self::TYPE_IDENTIFIER; + } + + $newSpec = str_replace('...', implode(', ', $newSpecParts), $this->specification); + + return array(array( + $newSpec, + array_merge($values, $this->columns), + $newSpecTypes, + )); + } +} diff --git a/library/Zend/Db/Sql/Ddl/CreateTable.php b/library/Zend/Db/Sql/Ddl/CreateTable.php new file mode 100755 index 0000000000..45bfd982d9 --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/CreateTable.php @@ -0,0 +1,218 @@ + 'CREATE %1$sTABLE %2$s (', + self::COLUMNS => array( + "\n %1\$s" => array( + array(1 => '%1$s', 'combinedby' => ",\n ") + ) + ), + self::CONSTRAINTS => array( + "\n %1\$s" => array( + array(1 => '%1$s', 'combinedby' => ",\n ") + ) + ), + ); + + /** + * @var string + */ + protected $table = ''; + + /** + * @param string $table + * @param bool $isTemporary + */ + public function __construct($table = '', $isTemporary = false) + { + $this->table = $table; + $this->setTemporary($isTemporary); + } + + /** + * @param bool $temporary + * @return self + */ + public function setTemporary($temporary) + { + $this->isTemporary = (bool) $temporary; + return $this; + } + + /** + * @return bool + */ + public function isTemporary() + { + return $this->isTemporary; + } + + /** + * @param string $name + * @return self + */ + public function setTable($name) + { + $this->table = $name; + return $this; + } + + /** + * @param Column\ColumnInterface $column + * @return self + */ + public function addColumn(Column\ColumnInterface $column) + { + $this->columns[] = $column; + return $this; + } + + /** + * @param Constraint\ConstraintInterface $constraint + * @return self + */ + public function addConstraint(Constraint\ConstraintInterface $constraint) + { + $this->constraints[] = $constraint; + return $this; + } + + /** + * @param string|null $key + * @return array + */ + public function getRawState($key = null) + { + $rawState = array( + self::COLUMNS => $this->columns, + self::CONSTRAINTS => $this->constraints, + self::TABLE => $this->table, + ); + + return (isset($key) && array_key_exists($key, $rawState)) ? $rawState[$key] : $rawState; + } + + /** + * @param PlatformInterface $adapterPlatform + * @return string + */ + public function getSqlString(PlatformInterface $adapterPlatform = null) + { + // get platform, or create default + $adapterPlatform = ($adapterPlatform) ?: new AdapterSql92Platform; + + $sqls = array(); + $parameters = array(); + + foreach ($this->specifications as $name => $specification) { + if (is_int($name)) { + $sqls[] = $specification; + continue; + } + + $parameters[$name] = $this->{'process' . $name}( + $adapterPlatform, + null, + null, + $sqls, + $parameters + ); + + + if ($specification + && is_array($parameters[$name]) + && ($parameters[$name] != array(array())) + ) { + $sqls[$name] = $this->createSqlFromSpecificationAndParameters( + $specification, + $parameters[$name] + ); + } + + if (stripos($name, 'table') === false + && $parameters[$name] !== array(array()) + ) { + $sqls[] = ",\n"; + } + } + + + // remove last , + if (count($sqls) > 2) { + array_pop($sqls); + } + + $sql = implode('', $sqls) . "\n)"; + + return $sql; + } + + protected function processTable(PlatformInterface $adapterPlatform = null) + { + $ret = array(); + if ($this->isTemporary) { + $ret[] = 'TEMPORARY '; + } else { + $ret[] = ''; + } + + $ret[] = $adapterPlatform->quoteIdentifier($this->table); + return $ret; + } + + protected function processColumns(PlatformInterface $adapterPlatform = null) + { + $sqls = array(); + foreach ($this->columns as $column) { + $sqls[] = $this->processExpression($column, $adapterPlatform)->getSql(); + } + return array($sqls); + } + + protected function processConstraints(PlatformInterface $adapterPlatform = null) + { + $sqls = array(); + foreach ($this->constraints as $constraint) { + $sqls[] = $this->processExpression($constraint, $adapterPlatform)->getSql(); + } + return array($sqls); + } +} diff --git a/library/Zend/Db/Sql/Ddl/DropTable.php b/library/Zend/Db/Sql/Ddl/DropTable.php new file mode 100755 index 0000000000..e38425c6bb --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/DropTable.php @@ -0,0 +1,77 @@ + 'DROP TABLE %1$s' + ); + + /** + * @var string + */ + protected $table = ''; + + /** + * @param string $table + */ + public function __construct($table = '') + { + $this->table = $table; + } + + /** + * @param null|PlatformInterface $adapterPlatform + * @return string + */ + public function getSqlString(PlatformInterface $adapterPlatform = null) + { + // get platform, or create default + $adapterPlatform = ($adapterPlatform) ?: new AdapterSql92Platform; + + $sqls = array(); + $parameters = array(); + + foreach ($this->specifications as $name => $specification) { + $parameters[$name] = $this->{'process' . $name}( + $adapterPlatform, + null, + null, + $sqls, + $parameters + ); + + if ($specification && is_array($parameters[$name])) { + $sqls[$name] = $this->createSqlFromSpecificationAndParameters( + $specification, + $parameters[$name] + ); + } + } + + $sql = implode(' ', $sqls); + return $sql; + } + + protected function processTable(PlatformInterface $adapterPlatform = null) + { + return array($adapterPlatform->quoteIdentifier($this->table)); + } +} diff --git a/library/Zend/Db/Sql/Ddl/SqlInterface.php b/library/Zend/Db/Sql/Ddl/SqlInterface.php new file mode 100755 index 0000000000..761312458a --- /dev/null +++ b/library/Zend/Db/Sql/Ddl/SqlInterface.php @@ -0,0 +1,16 @@ + 'DELETE FROM %1$s', + self::SPECIFICATION_WHERE => 'WHERE %1$s' + ); + + /** + * @var string|TableIdentifier + */ + protected $table = ''; + + /** + * @var bool + */ + protected $emptyWhereProtection = true; + + /** + * @var array + */ + protected $set = array(); + + /** + * @var null|string|Where + */ + protected $where = null; + + /** + * Constructor + * + * @param null|string|TableIdentifier $table + */ + public function __construct($table = null) + { + if ($table) { + $this->from($table); + } + $this->where = new Where(); + } + + /** + * Create from statement + * + * @param string|TableIdentifier $table + * @return Delete + */ + public function from($table) + { + $this->table = $table; + return $this; + } + + public function getRawState($key = null) + { + $rawState = array( + 'emptyWhereProtection' => $this->emptyWhereProtection, + 'table' => $this->table, + 'set' => $this->set, + 'where' => $this->where + ); + return (isset($key) && array_key_exists($key, $rawState)) ? $rawState[$key] : $rawState; + } + + /** + * Create where clause + * + * @param Where|\Closure|string|array $predicate + * @param string $combination One of the OP_* constants from Predicate\PredicateSet + * @return Delete + */ + public function where($predicate, $combination = Predicate\PredicateSet::OP_AND) + { + if ($predicate instanceof Where) { + $this->where = $predicate; + } else { + $this->where->addPredicates($predicate, $combination); + } + return $this; + } + + /** + * Prepare the delete statement + * + * @param AdapterInterface $adapter + * @param StatementContainerInterface $statementContainer + * @return void + */ + public function prepareStatement(AdapterInterface $adapter, StatementContainerInterface $statementContainer) + { + $driver = $adapter->getDriver(); + $platform = $adapter->getPlatform(); + $parameterContainer = $statementContainer->getParameterContainer(); + + if (!$parameterContainer instanceof ParameterContainer) { + $parameterContainer = new ParameterContainer(); + $statementContainer->setParameterContainer($parameterContainer); + } + + $table = $this->table; + $schema = null; + + // create quoted table name to use in delete processing + if ($table instanceof TableIdentifier) { + list($table, $schema) = $table->getTableAndSchema(); + } + + $table = $platform->quoteIdentifier($table); + + if ($schema) { + $table = $platform->quoteIdentifier($schema) . $platform->getIdentifierSeparator() . $table; + } + + $sql = sprintf($this->specifications[static::SPECIFICATION_DELETE], $table); + + // process where + if ($this->where->count() > 0) { + $whereParts = $this->processExpression($this->where, $platform, $driver, 'where'); + $parameterContainer->merge($whereParts->getParameterContainer()); + $sql .= ' ' . sprintf($this->specifications[static::SPECIFICATION_WHERE], $whereParts->getSql()); + } + $statementContainer->setSql($sql); + } + + /** + * Get the SQL string, based on the platform + * + * Platform defaults to Sql92 if none provided + * + * @param null|PlatformInterface $adapterPlatform + * @return string + */ + public function getSqlString(PlatformInterface $adapterPlatform = null) + { + $adapterPlatform = ($adapterPlatform) ?: new Sql92; + $table = $this->table; + $schema = null; + + // create quoted table name to use in delete processing + if ($table instanceof TableIdentifier) { + list($table, $schema) = $table->getTableAndSchema(); + } + + $table = $adapterPlatform->quoteIdentifier($table); + + if ($schema) { + $table = $adapterPlatform->quoteIdentifier($schema) . $adapterPlatform->getIdentifierSeparator() . $table; + } + + $sql = sprintf($this->specifications[static::SPECIFICATION_DELETE], $table); + + if ($this->where->count() > 0) { + $whereParts = $this->processExpression($this->where, $adapterPlatform, null, 'where'); + $sql .= ' ' . sprintf($this->specifications[static::SPECIFICATION_WHERE], $whereParts->getSql()); + } + + return $sql; + } + + /** + * Property overloading + * + * Overloads "where" only. + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + switch (strtolower($name)) { + case 'where': + return $this->where; + } + } +} diff --git a/library/Zend/Db/Sql/Exception/ExceptionInterface.php b/library/Zend/Db/Sql/Exception/ExceptionInterface.php new file mode 100755 index 0000000000..337266de87 --- /dev/null +++ b/library/Zend/Db/Sql/Exception/ExceptionInterface.php @@ -0,0 +1,16 @@ +setExpression($expression); + } + if ($parameters) { + $this->setParameters($parameters); + } + if ($types) { + $this->setTypes($types); + } + } + + /** + * @param $expression + * @return Expression + * @throws Exception\InvalidArgumentException + */ + public function setExpression($expression) + { + if (!is_string($expression) || $expression == '') { + throw new Exception\InvalidArgumentException('Supplied expression must be a string.'); + } + $this->expression = $expression; + return $this; + } + + /** + * @return string + */ + public function getExpression() + { + return $this->expression; + } + + /** + * @param $parameters + * @return Expression + * @throws Exception\InvalidArgumentException + */ + public function setParameters($parameters) + { + if (!is_scalar($parameters) && !is_array($parameters)) { + throw new Exception\InvalidArgumentException('Expression parameters must be a scalar or array.'); + } + $this->parameters = $parameters; + return $this; + } + + /** + * @return array + */ + public function getParameters() + { + return $this->parameters; + } + + /** + * @param array $types + * @return Expression + */ + public function setTypes(array $types) + { + $this->types = $types; + return $this; + } + + /** + * @return array + */ + public function getTypes() + { + return $this->types; + } + + /** + * @return array + * @throws Exception\RuntimeException + */ + public function getExpressionData() + { + $parameters = (is_scalar($this->parameters)) ? array($this->parameters) : $this->parameters; + + $types = array(); + $parametersCount = count($parameters); + + if ($parametersCount == 0 && strpos($this->expression, self::PLACEHOLDER) !== false) { + // if there are no parameters, but there is a placeholder + $parametersCount = substr_count($this->expression, self::PLACEHOLDER); + $parameters = array_fill(0, $parametersCount, null); + } + + for ($i = 0; $i < $parametersCount; $i++) { + $types[$i] = (isset($this->types[$i]) && ($this->types[$i] == self::TYPE_IDENTIFIER || $this->types[$i] == self::TYPE_LITERAL)) + ? $this->types[$i] : self::TYPE_VALUE; + } + + // assign locally, escaping % signs + $expression = str_replace('%', '%%', $this->expression); + + if ($parametersCount > 0) { + $count = 0; + $expression = str_replace(self::PLACEHOLDER, '%s', $expression, $count); + if ($count !== $parametersCount) { + throw new Exception\RuntimeException('The number of replacements in the expression does not match the number of parameters'); + } + } + + return array(array( + $expression, + $parameters, + $types + )); + } +} diff --git a/library/Zend/Db/Sql/ExpressionInterface.php b/library/Zend/Db/Sql/ExpressionInterface.php new file mode 100755 index 0000000000..99c9299392 --- /dev/null +++ b/library/Zend/Db/Sql/ExpressionInterface.php @@ -0,0 +1,36 @@ + 'INSERT INTO %1$s (%2$s) VALUES (%3$s)', + self::SPECIFICATION_SELECT => 'INSERT INTO %1$s %2$s %3$s', + ); + + /** + * @var string|TableIdentifier + */ + protected $table = null; + protected $columns = array(); + + /** + * @var array|Select + */ + protected $values = null; + + /** + * Constructor + * + * @param null|string|TableIdentifier $table + */ + public function __construct($table = null) + { + if ($table) { + $this->into($table); + } + } + + /** + * Create INTO clause + * + * @param string|TableIdentifier $table + * @return Insert + */ + public function into($table) + { + $this->table = $table; + return $this; + } + + /** + * Specify columns + * + * @param array $columns + * @return Insert + */ + public function columns(array $columns) + { + $this->columns = $columns; + return $this; + } + + /** + * Specify values to insert + * + * @param array|Select $values + * @param string $flag one of VALUES_MERGE or VALUES_SET; defaults to VALUES_SET + * @throws Exception\InvalidArgumentException + * @return Insert + */ + public function values($values, $flag = self::VALUES_SET) + { + if (!is_array($values) && !$values instanceof Select) { + throw new Exception\InvalidArgumentException('values() expects an array of values or Zend\Db\Sql\Select instance'); + } + + if ($values instanceof Select) { + if ($flag == self::VALUES_MERGE && (is_array($this->values) && !empty($this->values))) { + throw new Exception\InvalidArgumentException( + 'A Zend\Db\Sql\Select instance cannot be provided with the merge flag when values already exist.' + ); + } + $this->values = $values; + return $this; + } + + // determine if this is assoc or a set of values + $keys = array_keys($values); + $firstKey = current($keys); + + if ($flag == self::VALUES_SET) { + $this->columns = array(); + $this->values = array(); + } elseif ($this->values instanceof Select) { + throw new Exception\InvalidArgumentException( + 'An array of values cannot be provided with the merge flag when a Zend\Db\Sql\Select' + . ' instance already exists as the value source.' + ); + } + + if (is_string($firstKey)) { + foreach ($keys as $key) { + if (($index = array_search($key, $this->columns)) !== false) { + $this->values[$index] = $values[$key]; + } else { + $this->columns[] = $key; + $this->values[] = $values[$key]; + } + } + } elseif (is_int($firstKey)) { + // determine if count of columns should match count of values + $this->values = array_merge($this->values, array_values($values)); + } + + return $this; + } + + /** + * Create INTO SELECT clause + * + * @param Select $select + * @return self + */ + public function select(Select $select) + { + return $this->values($select); + } + + /** + * Get raw state + * + * @param string $key + * @return mixed + */ + public function getRawState($key = null) + { + $rawState = array( + 'table' => $this->table, + 'columns' => $this->columns, + 'values' => $this->values + ); + return (isset($key) && array_key_exists($key, $rawState)) ? $rawState[$key] : $rawState; + } + + /** + * Prepare statement + * + * @param AdapterInterface $adapter + * @param StatementContainerInterface $statementContainer + * @return void + */ + public function prepareStatement(AdapterInterface $adapter, StatementContainerInterface $statementContainer) + { + $driver = $adapter->getDriver(); + $platform = $adapter->getPlatform(); + $parameterContainer = $statementContainer->getParameterContainer(); + + if (!$parameterContainer instanceof ParameterContainer) { + $parameterContainer = new ParameterContainer(); + $statementContainer->setParameterContainer($parameterContainer); + } + + $table = $this->table; + $schema = null; + + // create quoted table name to use in insert processing + if ($table instanceof TableIdentifier) { + list($table, $schema) = $table->getTableAndSchema(); + } + + $table = $platform->quoteIdentifier($table); + + if ($schema) { + $table = $platform->quoteIdentifier($schema) . $platform->getIdentifierSeparator() . $table; + } + + $columns = array(); + $values = array(); + + if (is_array($this->values)) { + foreach ($this->columns as $cIndex => $column) { + $columns[$cIndex] = $platform->quoteIdentifier($column); + if (isset($this->values[$cIndex]) && $this->values[$cIndex] instanceof Expression) { + $exprData = $this->processExpression($this->values[$cIndex], $platform, $driver); + $values[$cIndex] = $exprData->getSql(); + $parameterContainer->merge($exprData->getParameterContainer()); + } else { + $values[$cIndex] = $driver->formatParameterName($column); + if (isset($this->values[$cIndex])) { + $parameterContainer->offsetSet($column, $this->values[$cIndex]); + } else { + $parameterContainer->offsetSet($column, null); + } + } + } + $sql = sprintf( + $this->specifications[static::SPECIFICATION_INSERT], + $table, + implode(', ', $columns), + implode(', ', $values) + ); + } elseif ($this->values instanceof Select) { + $this->values->prepareStatement($adapter, $statementContainer); + + $columns = array_map(array($platform, 'quoteIdentifier'), $this->columns); + $columns = implode(', ', $columns); + + $sql = sprintf( + $this->specifications[static::SPECIFICATION_SELECT], + $table, + $columns ? "($columns)" : "", + $statementContainer->getSql() + ); + } else { + throw new Exception\InvalidArgumentException('values or select should be present'); + } + $statementContainer->setSql($sql); + } + + /** + * Get SQL string for this statement + * + * @param null|PlatformInterface $adapterPlatform Defaults to Sql92 if none provided + * @return string + */ + public function getSqlString(PlatformInterface $adapterPlatform = null) + { + $adapterPlatform = ($adapterPlatform) ?: new Sql92; + $table = $this->table; + $schema = null; + + // create quoted table name to use in insert processing + if ($table instanceof TableIdentifier) { + list($table, $schema) = $table->getTableAndSchema(); + } + + $table = $adapterPlatform->quoteIdentifier($table); + + if ($schema) { + $table = $adapterPlatform->quoteIdentifier($schema) . $adapterPlatform->getIdentifierSeparator() . $table; + } + + $columns = array_map(array($adapterPlatform, 'quoteIdentifier'), $this->columns); + $columns = implode(', ', $columns); + + if (is_array($this->values)) { + $values = array(); + foreach ($this->values as $value) { + if ($value instanceof Expression) { + $exprData = $this->processExpression($value, $adapterPlatform); + $values[] = $exprData->getSql(); + } elseif ($value === null) { + $values[] = 'NULL'; + } else { + $values[] = $adapterPlatform->quoteValue($value); + } + } + return sprintf( + $this->specifications[static::SPECIFICATION_INSERT], + $table, + $columns, + implode(', ', $values) + ); + } elseif ($this->values instanceof Select) { + $selectString = $this->values->getSqlString($adapterPlatform); + if ($columns) { + $columns = "($columns)"; + } + return sprintf( + $this->specifications[static::SPECIFICATION_SELECT], + $table, + $columns, + $selectString + ); + } else { + throw new Exception\InvalidArgumentException('values or select should be present'); + } + } + + /** + * Overloading: variable setting + * + * Proxies to values, using VALUES_MERGE strategy + * + * @param string $name + * @param mixed $value + * @return Insert + */ + public function __set($name, $value) + { + $values = array($name => $value); + $this->values($values, self::VALUES_MERGE); + return $this; + } + + /** + * Overloading: variable unset + * + * Proxies to values and columns + * + * @param string $name + * @throws Exception\InvalidArgumentException + * @return void + */ + public function __unset($name) + { + if (($position = array_search($name, $this->columns)) === false) { + throw new Exception\InvalidArgumentException('The key ' . $name . ' was not found in this objects column list'); + } + + unset($this->columns[$position]); + if (is_array($this->values)) { + unset($this->values[$position]); + } + } + + /** + * Overloading: variable isset + * + * Proxies to columns; does a column of that name exist? + * + * @param string $name + * @return bool + */ + public function __isset($name) + { + return in_array($name, $this->columns); + } + + /** + * Overloading: variable retrieval + * + * Retrieves value by column name + * + * @param string $name + * @throws Exception\InvalidArgumentException + * @return mixed + */ + public function __get($name) + { + if (!is_array($this->values)) { + return null; + } + if (($position = array_search($name, $this->columns)) === false) { + throw new Exception\InvalidArgumentException('The key ' . $name . ' was not found in this objects column list'); + } + return $this->values[$position]; + } +} diff --git a/library/Zend/Db/Sql/Literal.php b/library/Zend/Db/Sql/Literal.php new file mode 100755 index 0000000000..ba67415a3d --- /dev/null +++ b/library/Zend/Db/Sql/Literal.php @@ -0,0 +1,56 @@ +literal = $literal; + } + + /** + * @param string $literal + * @return Literal + */ + public function setLiteral($literal) + { + $this->literal = $literal; + return $this; + } + + /** + * @return string + */ + public function getLiteral() + { + return $this->literal; + } + + /** + * @return array + */ + public function getExpressionData() + { + return array(array( + str_replace('%', '%%', $this->literal), + array(), + array() + )); + } +} diff --git a/library/Zend/Db/Sql/Platform/AbstractPlatform.php b/library/Zend/Db/Sql/Platform/AbstractPlatform.php new file mode 100755 index 0000000000..c5ddd6ce60 --- /dev/null +++ b/library/Zend/Db/Sql/Platform/AbstractPlatform.php @@ -0,0 +1,110 @@ +subject = $subject; + } + + /** + * @param $type + * @param PlatformDecoratorInterface $decorator + */ + public function setTypeDecorator($type, PlatformDecoratorInterface $decorator) + { + $this->decorators[$type] = $decorator; + } + + /** + * @return array|PlatformDecoratorInterface[] + */ + public function getDecorators() + { + return $this->decorators; + } + + /** + * @param AdapterInterface $adapter + * @param StatementContainerInterface $statementContainer + * @throws Exception\RuntimeException + * @return void + */ + public function prepareStatement(AdapterInterface $adapter, StatementContainerInterface $statementContainer) + { + if (!$this->subject instanceof PreparableSqlInterface) { + throw new Exception\RuntimeException('The subject does not appear to implement Zend\Db\Sql\PreparableSqlInterface, thus calling prepareStatement() has no effect'); + } + + $decoratorForType = false; + foreach ($this->decorators as $type => $decorator) { + if ($this->subject instanceof $type && $decorator instanceof PreparableSqlInterface) { + /** @var $decoratorForType PreparableSqlInterface|PlatformDecoratorInterface */ + $decoratorForType = $decorator; + break; + } + } + if ($decoratorForType) { + $decoratorForType->setSubject($this->subject); + $decoratorForType->prepareStatement($adapter, $statementContainer); + } else { + $this->subject->prepareStatement($adapter, $statementContainer); + } + } + + /** + * @param null|\Zend\Db\Adapter\Platform\PlatformInterface $adapterPlatform + * @return mixed + * @throws Exception\RuntimeException + */ + public function getSqlString(PlatformInterface $adapterPlatform = null) + { + if (!$this->subject instanceof SqlInterface) { + throw new Exception\RuntimeException('The subject does not appear to implement Zend\Db\Sql\PreparableSqlInterface, thus calling prepareStatement() has no effect'); + } + + $decoratorForType = false; + foreach ($this->decorators as $type => $decorator) { + if ($this->subject instanceof $type && $decorator instanceof SqlInterface) { + /** @var $decoratorForType SqlInterface|PlatformDecoratorInterface */ + $decoratorForType = $decorator; + break; + } + } + if ($decoratorForType) { + $decoratorForType->setSubject($this->subject); + return $decoratorForType->getSqlString($adapterPlatform); + } + + return $this->subject->getSqlString($adapterPlatform); + } +} diff --git a/library/Zend/Db/Sql/Platform/IbmDb2/IbmDb2.php b/library/Zend/Db/Sql/Platform/IbmDb2/IbmDb2.php new file mode 100755 index 0000000000..f744bb470f --- /dev/null +++ b/library/Zend/Db/Sql/Platform/IbmDb2/IbmDb2.php @@ -0,0 +1,23 @@ +setTypeDecorator('Zend\Db\Sql\Select', ($selectDecorator) ?: new SelectDecorator()); + } +} diff --git a/library/Zend/Db/Sql/Platform/IbmDb2/SelectDecorator.php b/library/Zend/Db/Sql/Platform/IbmDb2/SelectDecorator.php new file mode 100755 index 0000000000..4ad3236940 --- /dev/null +++ b/library/Zend/Db/Sql/Platform/IbmDb2/SelectDecorator.php @@ -0,0 +1,192 @@ +isSelectContainDistinct; + } + + /** + * @param boolean $isSelectContainDistinct + */ + public function setIsSelectContainDistinct($isSelectContainDistinct) + { + $this->isSelectContainDistinct = $isSelectContainDistinct; + } + + /** + * @param Select $select + */ + public function setSubject($select) + { + $this->select = $select; + } + + /** + * @see Select::renderTable + */ + protected function renderTable($table, $alias = null) + { + return $table . ' ' . $alias; + } + + /** + * @param AdapterInterface $adapter + * @param StatementContainerInterface $statementContainer + */ + public function prepareStatement(AdapterInterface $adapter, StatementContainerInterface $statementContainer) + { + // localize variables + foreach (get_object_vars($this->select) as $name => $value) { + $this->{$name} = $value; + } + // set specifications + unset($this->specifications[self::LIMIT]); + unset($this->specifications[self::OFFSET]); + + $this->specifications['LIMITOFFSET'] = null; + parent::prepareStatement($adapter, $statementContainer); + } + + /** + * @param PlatformInterface $platform + * @return string + */ + public function getSqlString(PlatformInterface $platform = null) + { + // localize variables + foreach (get_object_vars($this->select) as $name => $value) { + $this->{$name} = $value; + } + + unset($this->specifications[self::LIMIT]); + unset($this->specifications[self::OFFSET]); + $this->specifications['LIMITOFFSET'] = null; + + return parent::getSqlString($platform); + } + + /** + * @param PlatformInterface $platform + * @param DriverInterface $driver + * @param ParameterContainer $parameterContainer + * @param array $sqls + * @param array $parameters + */ + protected function processLimitOffset(PlatformInterface $platform, DriverInterface $driver = null, ParameterContainer $parameterContainer = null, &$sqls, &$parameters) + { + if ($this->limit === null && $this->offset === null) { + return; + } + + $selectParameters = $parameters[self::SELECT]; + + $starSuffix = $platform->getIdentifierSeparator() . self::SQL_STAR; + foreach ($selectParameters[0] as $i => $columnParameters) { + if ($columnParameters[0] == self::SQL_STAR + || (isset($columnParameters[1]) && $columnParameters[1] == self::SQL_STAR) + || strpos($columnParameters[0], $starSuffix) + ) { + $selectParameters[0] = array(array(self::SQL_STAR)); + break; + } + + if (isset($columnParameters[1])) { + array_shift($columnParameters); + $selectParameters[0][$i] = $columnParameters; + } + } + + // first, produce column list without compound names (using the AS portion only) + array_unshift($sqls, $this->createSqlFromSpecificationAndParameters( + array('SELECT %1$s FROM (' => current($this->specifications[self::SELECT])), + $selectParameters + )); + + if (preg_match('/DISTINCT/i', $sqls[0])) { + $this->setIsSelectContainDistinct(true); + } + + if ($parameterContainer) { + // create bottom part of query, with offset and limit using row_number + $limitParamName = $driver->formatParameterName('limit'); + $offsetParamName = $driver->formatParameterName('offset'); + + array_push($sqls, sprintf( + ") AS ZEND_IBMDB2_SERVER_LIMIT_OFFSET_EMULATION WHERE ZEND_IBMDB2_SERVER_LIMIT_OFFSET_EMULATION.ZEND_DB_ROWNUM BETWEEN %s AND %s", + $offsetParamName, + $limitParamName + )); + + if ((int) $this->offset > 0) { + $parameterContainer->offsetSet('offset', (int) $this->offset + 1); + } else { + $parameterContainer->offsetSet('offset', (int) $this->offset); + } + + $parameterContainer->offsetSet('limit', (int) $this->limit + (int) $this->offset); + } else { + if ((int) $this->offset > 0) { + $offset = (int) $this->offset + 1; + } else { + $offset = (int) $this->offset; + } + + array_push($sqls, sprintf( + ") AS ZEND_IBMDB2_SERVER_LIMIT_OFFSET_EMULATION WHERE ZEND_IBMDB2_SERVER_LIMIT_OFFSET_EMULATION.ZEND_DB_ROWNUM BETWEEN %d AND %d", + $offset, + (int) $this->limit + (int) $this->offset + )); + } + + if (isset($sqls[self::ORDER])) { + $orderBy = $sqls[self::ORDER]; + unset($sqls[self::ORDER]); + } else { + $orderBy = ''; + } + + // add a column for row_number() using the order specification //dense_rank() + if ($this->getIsSelectContainDistinct()) { + $parameters[self::SELECT][0][] = array('DENSE_RANK() OVER (' . $orderBy . ')', 'ZEND_DB_ROWNUM'); + } else { + $parameters[self::SELECT][0][] = array('ROW_NUMBER() OVER (' . $orderBy . ')', 'ZEND_DB_ROWNUM'); + } + + $sqls[self::SELECT] = $this->createSqlFromSpecificationAndParameters( + $this->specifications[self::SELECT], + $parameters[self::SELECT] + ); + } +} diff --git a/library/Zend/Db/Sql/Platform/Mysql/Ddl/CreateTableDecorator.php b/library/Zend/Db/Sql/Platform/Mysql/Ddl/CreateTableDecorator.php new file mode 100755 index 0000000000..d9cfa15563 --- /dev/null +++ b/library/Zend/Db/Sql/Platform/Mysql/Ddl/CreateTableDecorator.php @@ -0,0 +1,86 @@ +createTable = $subject; + } + + /** + * @param null|PlatformInterface $platform + * @return string + */ + public function getSqlString(PlatformInterface $platform = null) + { + // localize variables + foreach (get_object_vars($this->createTable) as $name => $value) { + $this->{$name} = $value; + } + return parent::getSqlString($platform); + } + + protected function processColumns(PlatformInterface $platform = null) + { + $sqls = array(); + foreach ($this->columns as $i => $column) { + $stmtContainer = $this->processExpression($column, $platform); + $sql = $stmtContainer->getSql(); + $columnOptions = $column->getOptions(); + + foreach ($columnOptions as $coName => $coValue) { + switch (strtolower(str_replace(array('-', '_', ' '), '', $coName))) { + case 'identity': + case 'serial': + case 'autoincrement': + $sql .= ' AUTO_INCREMENT'; + break; + /* + case 'primary': + case 'primarykey': + $sql .= ' PRIMARY KEY'; + break; + case 'unique': + case 'uniquekey': + $sql .= ' UNIQUE KEY'; + break; + */ + case 'comment': + $sql .= ' COMMENT \'' . $coValue . '\''; + break; + case 'columnformat': + case 'format': + $sql .= ' COLUMN_FORMAT ' . strtoupper($coValue); + break; + case 'storage': + $sql .= ' STORAGE ' . strtoupper($coValue); + break; + } + } + $stmtContainer->setSql($sql); + $sqls[$i] = $stmtContainer; + } + return array($sqls); + } +} diff --git a/library/Zend/Db/Sql/Platform/Mysql/Mysql.php b/library/Zend/Db/Sql/Platform/Mysql/Mysql.php new file mode 100755 index 0000000000..80455869a4 --- /dev/null +++ b/library/Zend/Db/Sql/Platform/Mysql/Mysql.php @@ -0,0 +1,21 @@ +setTypeDecorator('Zend\Db\Sql\Select', new SelectDecorator()); + $this->setTypeDecorator('Zend\Db\Sql\Ddl\CreateTable', new Ddl\CreateTableDecorator()); + } +} diff --git a/library/Zend/Db/Sql/Platform/Mysql/SelectDecorator.php b/library/Zend/Db/Sql/Platform/Mysql/SelectDecorator.php new file mode 100755 index 0000000000..5c4678a730 --- /dev/null +++ b/library/Zend/Db/Sql/Platform/Mysql/SelectDecorator.php @@ -0,0 +1,97 @@ +select = $select; + } + + /** + * @param AdapterInterface $adapter + * @param StatementContainerInterface $statementContainer + */ + public function prepareStatement(AdapterInterface $adapter, StatementContainerInterface $statementContainer) + { + // localize variables + foreach (get_object_vars($this->select) as $name => $value) { + $this->{$name} = $value; + } + if ($this->limit === null && $this->offset !== null) { + $this->specifications[self::LIMIT] = 'LIMIT 18446744073709551615'; + } + parent::prepareStatement($adapter, $statementContainer); + } + + /** + * @param PlatformInterface $platform + * @return string + */ + public function getSqlString(PlatformInterface $platform = null) + { + // localize variables + foreach (get_object_vars($this->select) as $name => $value) { + $this->{$name} = $value; + } + if ($this->limit === null && $this->offset !== null) { + $this->specifications[self::LIMIT] = 'LIMIT 18446744073709551615'; + } + return parent::getSqlString($platform); + } + + protected function processLimit(PlatformInterface $platform, DriverInterface $driver = null, ParameterContainer $parameterContainer = null) + { + if ($this->limit === null && $this->offset !== null) { + return array(''); + } + if ($this->limit === null) { + return null; + } + if ($driver) { + $sql = $driver->formatParameterName('limit'); + $parameterContainer->offsetSet('limit', $this->limit, ParameterContainer::TYPE_INTEGER); + } else { + $sql = $this->limit; + } + + return array($sql); + } + + protected function processOffset(PlatformInterface $platform, DriverInterface $driver = null, ParameterContainer $parameterContainer = null) + { + if ($this->offset === null) { + return null; + } + if ($driver) { + $parameterContainer->offsetSet('offset', $this->offset, ParameterContainer::TYPE_INTEGER); + return array($driver->formatParameterName('offset')); + } + + return array($this->offset); + } +} diff --git a/library/Zend/Db/Sql/Platform/Oracle/Oracle.php b/library/Zend/Db/Sql/Platform/Oracle/Oracle.php new file mode 100755 index 0000000000..e5af22fa92 --- /dev/null +++ b/library/Zend/Db/Sql/Platform/Oracle/Oracle.php @@ -0,0 +1,20 @@ +setTypeDecorator('Zend\Db\Sql\Select', ($selectDecorator) ?: new SelectDecorator()); + } +} diff --git a/library/Zend/Db/Sql/Platform/Oracle/SelectDecorator.php b/library/Zend/Db/Sql/Platform/Oracle/SelectDecorator.php new file mode 100755 index 0000000000..457c696bb9 --- /dev/null +++ b/library/Zend/Db/Sql/Platform/Oracle/SelectDecorator.php @@ -0,0 +1,180 @@ +select = $select; + } + + /** + * @see \Zend\Db\Sql\Select::renderTable + */ + protected function renderTable($table, $alias = null) + { + return $table . ' ' . $alias; + } + + /** + * @param AdapterInterface $adapter + * @param StatementContainerInterface $statementContainer + */ + public function prepareStatement(AdapterInterface $adapter, StatementContainerInterface $statementContainer) + { + // localize variables + foreach (get_object_vars($this->select) as $name => $value) { + $this->{$name} = $value; + } + // set specifications + unset($this->specifications[self::LIMIT]); + unset($this->specifications[self::OFFSET]); + + $this->specifications['LIMITOFFSET'] = null; + parent::prepareStatement($adapter, $statementContainer); + } + + /** + * @param PlatformInterface $platform + * @return string + */ + public function getSqlString(PlatformInterface $platform = null) + { + // localize variables + foreach (get_object_vars($this->select) as $name => $value) { + $this->{$name} = $value; + } + + // set specifications + unset($this->specifications[self::LIMIT]); + unset($this->specifications[self::OFFSET]); + + $this->specifications['LIMITOFFSET'] = null; + return parent::getSqlString($platform); + } + + /** + * @param PlatformInterface $platform + * @param DriverInterface $driver + * @param ParameterContainer $parameterContainer + * @param $sqls + * @param $parameters + * @return null + */ + protected function processLimitOffset(PlatformInterface $platform, DriverInterface $driver = null, ParameterContainer $parameterContainer = null, &$sqls, &$parameters) + { + if ($this->limit === null && $this->offset === null) { + return null; + } + + $selectParameters = $parameters[self::SELECT]; + + $starSuffix = $platform->getIdentifierSeparator() . self::SQL_STAR; + foreach ($selectParameters[0] as $i => $columnParameters) { + if ($columnParameters[0] == self::SQL_STAR || (isset($columnParameters[1]) && $columnParameters[1] == self::SQL_STAR) || strpos($columnParameters[0], $starSuffix)) { + $selectParameters[0] = array(array(self::SQL_STAR)); + break; + } + if (isset($columnParameters[1])) { + array_shift($columnParameters); + $selectParameters[0][$i] = $columnParameters; + } + } + + if ($this->offset === null) { + $this->offset = 0; + } + + // first, produce column list without compound names (using the AS portion only) + array_unshift($sqls, $this->createSqlFromSpecificationAndParameters( + array('SELECT %1$s FROM (SELECT b.%1$s, rownum b_rownum FROM (' => current($this->specifications[self::SELECT])), $selectParameters + )); + + if ($parameterContainer) { + if ($this->limit === null) { + array_push($sqls, ') b ) WHERE b_rownum > (:offset)'); + $parameterContainer->offsetSet('offset', $this->offset, $parameterContainer::TYPE_INTEGER); + } else { + // create bottom part of query, with offset and limit using row_number + array_push($sqls, ') b WHERE rownum <= (:offset+:limit)) WHERE b_rownum >= (:offset + 1)'); + $parameterContainer->offsetSet('offset', $this->offset, $parameterContainer::TYPE_INTEGER); + $parameterContainer->offsetSet('limit', $this->limit, $parameterContainer::TYPE_INTEGER); + } + } else { + if ($this->limit === null) { + array_push($sqls, ') b ) WHERE b_rownum > ('. (int) $this->offset. ')' + ); + } else { + array_push($sqls, ') b WHERE rownum <= (' + . (int) $this->offset + . '+' + . (int) $this->limit + . ')) WHERE b_rownum >= (' + . (int) $this->offset + . ' + 1)' + ); + } + } + + $sqls[self::SELECT] = $this->createSqlFromSpecificationAndParameters( + $this->specifications[self::SELECT], $parameters[self::SELECT] + ); + } + + + protected function processJoins(PlatformInterface $platform, DriverInterface $driver = null, ParameterContainer $parameterContainer = null) + { + if (!$this->joins) { + return null; + } + + // process joins + $joinSpecArgArray = array(); + foreach ($this->joins as $j => $join) { + $joinSpecArgArray[$j] = array(); + // type + $joinSpecArgArray[$j][] = strtoupper($join['type']); + // table name + $joinSpecArgArray[$j][] = (is_array($join['name'])) + ? $platform->quoteIdentifier(current($join['name'])) . ' ' . $platform->quoteIdentifier(key($join['name'])) + : $platform->quoteIdentifier($join['name']); + // on expression + $joinSpecArgArray[$j][] = ($join['on'] instanceof ExpressionInterface) + ? $this->processExpression($join['on'], $platform, $driver, $this->processInfo['paramPrefix'] . 'join') + : $platform->quoteIdentifierInFragment($join['on'], array('=', 'AND', 'OR', '(', ')', 'BETWEEN')); // on + if ($joinSpecArgArray[$j][2] instanceof StatementContainerInterface) { + if ($parameterContainer) { + $parameterContainer->merge($joinSpecArgArray[$j][2]->getParameterContainer()); + } + $joinSpecArgArray[$j][2] = $joinSpecArgArray[$j][2]->getSql(); + } + } + + return array($joinSpecArgArray); + } +} diff --git a/library/Zend/Db/Sql/Platform/Platform.php b/library/Zend/Db/Sql/Platform/Platform.php new file mode 100755 index 0000000000..4a6fc2e985 --- /dev/null +++ b/library/Zend/Db/Sql/Platform/Platform.php @@ -0,0 +1,46 @@ +adapter = $adapter; + $platform = $adapter->getPlatform(); + switch (strtolower($platform->getName())) { + case 'mysql': + $platform = new Mysql\Mysql(); + $this->decorators = $platform->decorators; + break; + case 'sqlserver': + $platform = new SqlServer\SqlServer(); + $this->decorators = $platform->decorators; + break; + case 'oracle': + $platform = new Oracle\Oracle(); + $this->decorators = $platform->decorators; + break; + case 'ibm db2': + case 'ibm_db2': + case 'ibmdb2': + $platform = new IbmDb2\IbmDb2(); + $this->decorators = $platform->decorators; + default: + } + } +} diff --git a/library/Zend/Db/Sql/Platform/PlatformDecoratorInterface.php b/library/Zend/Db/Sql/Platform/PlatformDecoratorInterface.php new file mode 100755 index 0000000000..2ff7c97ce6 --- /dev/null +++ b/library/Zend/Db/Sql/Platform/PlatformDecoratorInterface.php @@ -0,0 +1,15 @@ +createTable = $subject; + return $this; + } + + /** + * @param null|PlatformInterface $platform + * @return string + */ + public function getSqlString(PlatformInterface $platform = null) + { + // localize variables + foreach (get_object_vars($this->createTable) as $name => $value) { + $this->{$name} = $value; + } + return parent::getSqlString($platform); + } + + /** + * @param PlatformInterface $adapterPlatform + * @return array + */ + protected function processTable(PlatformInterface $adapterPlatform = null) + { + $ret = array(''); + if ($this->isTemporary) { + $table = '#'; + } else { + $table = ''; + } + $ret[] = $adapterPlatform->quoteIdentifier($table . ltrim($this->table, '#')); + return $ret; + } +} diff --git a/library/Zend/Db/Sql/Platform/SqlServer/SelectDecorator.php b/library/Zend/Db/Sql/Platform/SqlServer/SelectDecorator.php new file mode 100755 index 0000000000..7667ebcbda --- /dev/null +++ b/library/Zend/Db/Sql/Platform/SqlServer/SelectDecorator.php @@ -0,0 +1,145 @@ +select = $select; + } + + /** + * @param AdapterInterface $adapter + * @param StatementContainerInterface $statementContainer + */ + public function prepareStatement(AdapterInterface $adapter, StatementContainerInterface $statementContainer) + { + // localize variables + foreach (get_object_vars($this->select) as $name => $value) { + $this->{$name} = $value; + } + + // set specifications + unset($this->specifications[self::LIMIT]); + unset($this->specifications[self::OFFSET]); + + $this->specifications['LIMITOFFSET'] = null; + parent::prepareStatement($adapter, $statementContainer); + + //set statement cursor type + if ($statementContainer instanceof Statement) { + $statementContainer->setPrepareOptions(array('Scrollable'=>\SQLSRV_CURSOR_STATIC)); + } + } + + /** + * @param PlatformInterface $platform + * @return string + */ + public function getSqlString(PlatformInterface $platform = null) + { + // localize variables + foreach (get_object_vars($this->select) as $name => $value) { + $this->{$name} = $value; + } + + // set specifications + unset($this->specifications[self::LIMIT]); + unset($this->specifications[self::OFFSET]); + + $this->specifications['LIMITOFFSET'] = null; + return parent::getSqlString($platform); + } + + /** + * @param PlatformInterface $platform + * @param DriverInterface $driver + * @param ParameterContainer $parameterContainer + * @param $sqls + * @param $parameters + * @return null + */ + protected function processLimitOffset(PlatformInterface $platform, DriverInterface $driver = null, ParameterContainer $parameterContainer = null, &$sqls, &$parameters) + { + if ($this->limit === null && $this->offset === null) { + return null; + } + + $selectParameters = $parameters[self::SELECT]; + + $starSuffix = $platform->getIdentifierSeparator() . self::SQL_STAR; + foreach ($selectParameters[0] as $i => $columnParameters) { + if ($columnParameters[0] == self::SQL_STAR || (isset($columnParameters[1]) && $columnParameters[1] == self::SQL_STAR) || strpos($columnParameters[0], $starSuffix)) { + $selectParameters[0] = array(array(self::SQL_STAR)); + break; + } + if (isset($columnParameters[1])) { + array_shift($columnParameters); + $selectParameters[0][$i] = $columnParameters; + } + } + + // first, produce column list without compound names (using the AS portion only) + array_unshift($sqls, $this->createSqlFromSpecificationAndParameters( + array('SELECT %1$s FROM (' => current($this->specifications[self::SELECT])), + $selectParameters + )); + + if ($parameterContainer) { + // create bottom part of query, with offset and limit using row_number + $limitParamName = $driver->formatParameterName('limit'); + $offsetParamName = $driver->formatParameterName('offset'); + $offsetForSumParamName = $driver->formatParameterName('offsetForSum'); + array_push($sqls, ') AS [ZEND_SQL_SERVER_LIMIT_OFFSET_EMULATION] WHERE [ZEND_SQL_SERVER_LIMIT_OFFSET_EMULATION].[__ZEND_ROW_NUMBER] BETWEEN ' + . $offsetParamName . '+1 AND ' . $limitParamName . '+' . $offsetForSumParamName); + $parameterContainer->offsetSet('offset', $this->offset); + $parameterContainer->offsetSet('limit', $this->limit); + $parameterContainer->offsetSetReference('offsetForSum', 'offset'); + } else { + array_push($sqls, ') AS [ZEND_SQL_SERVER_LIMIT_OFFSET_EMULATION] WHERE [ZEND_SQL_SERVER_LIMIT_OFFSET_EMULATION].[__ZEND_ROW_NUMBER] BETWEEN ' + . (int) $this->offset . '+1 AND ' + . (int) $this->limit . '+' . (int) $this->offset + ); + } + + if (isset($sqls[self::ORDER])) { + $orderBy = $sqls[self::ORDER]; + unset($sqls[self::ORDER]); + } else { + $orderBy = 'ORDER BY (SELECT 1)'; + } + + // add a column for row_number() using the order specification + $parameters[self::SELECT][0][] = array('ROW_NUMBER() OVER (' . $orderBy . ')', '[__ZEND_ROW_NUMBER]'); + + $sqls[self::SELECT] = $this->createSqlFromSpecificationAndParameters( + $this->specifications[self::SELECT], + $parameters[self::SELECT] + ); + } +} diff --git a/library/Zend/Db/Sql/Platform/SqlServer/SqlServer.php b/library/Zend/Db/Sql/Platform/SqlServer/SqlServer.php new file mode 100755 index 0000000000..ed72b77aa5 --- /dev/null +++ b/library/Zend/Db/Sql/Platform/SqlServer/SqlServer.php @@ -0,0 +1,21 @@ +setTypeDecorator('Zend\Db\Sql\Select', ($selectDecorator) ?: new SelectDecorator()); + $this->setTypeDecorator('Zend\Db\Sql\Ddl\CreateTable', new Ddl\CreateTableDecorator()); + } +} diff --git a/library/Zend/Db/Sql/Predicate/Between.php b/library/Zend/Db/Sql/Predicate/Between.php new file mode 100755 index 0000000000..686b65db58 --- /dev/null +++ b/library/Zend/Db/Sql/Predicate/Between.php @@ -0,0 +1,142 @@ +setIdentifier($identifier); + } + if ($minValue !== null) { + $this->setMinValue($minValue); + } + if ($maxValue !== null) { + $this->setMaxValue($maxValue); + } + } + + /** + * Set identifier for comparison + * + * @param string $identifier + * @return Between + */ + public function setIdentifier($identifier) + { + $this->identifier = $identifier; + return $this; + } + + /** + * Get identifier of comparison + * + * @return null|string + */ + public function getIdentifier() + { + return $this->identifier; + } + + /** + * Set minimum boundary for comparison + * + * @param int|float|string $minValue + * @return Between + */ + public function setMinValue($minValue) + { + $this->minValue = $minValue; + return $this; + } + + /** + * Get minimum boundary for comparison + * + * @return null|int|float|string + */ + public function getMinValue() + { + return $this->minValue; + } + + /** + * Set maximum boundary for comparison + * + * @param int|float|string $maxValue + * @return Between + */ + public function setMaxValue($maxValue) + { + $this->maxValue = $maxValue; + return $this; + } + + /** + * Get maximum boundary for comparison + * + * @return null|int|float|string + */ + public function getMaxValue() + { + return $this->maxValue; + } + + /** + * Set specification string to use in forming SQL predicate + * + * @param string $specification + * @return Between + */ + public function setSpecification($specification) + { + $this->specification = $specification; + return $this; + } + + /** + * Get specification string to use in forming SQL predicate + * + * @return string + */ + public function getSpecification() + { + return $this->specification; + } + + /** + * Return "where" parts + * + * @return array + */ + public function getExpressionData() + { + return array( + array( + $this->getSpecification(), + array($this->identifier, $this->minValue, $this->maxValue), + array(self::TYPE_IDENTIFIER, self::TYPE_VALUE, self::TYPE_VALUE), + ), + ); + } +} diff --git a/library/Zend/Db/Sql/Predicate/Expression.php b/library/Zend/Db/Sql/Predicate/Expression.php new file mode 100755 index 0000000000..4822dd0815 --- /dev/null +++ b/library/Zend/Db/Sql/Predicate/Expression.php @@ -0,0 +1,41 @@ +setExpression($expression); + } + + if (is_array($valueParameter)) { + $this->setParameters($valueParameter); + } else { + $argNum = func_num_args(); + if ($argNum > 2 || is_scalar($valueParameter)) { + $parameters = array(); + for ($i = 1; $i < $argNum; $i++) { + $parameters[] = func_get_arg($i); + } + $this->setParameters($parameters); + } + } + } +} diff --git a/library/Zend/Db/Sql/Predicate/In.php b/library/Zend/Db/Sql/Predicate/In.php new file mode 100755 index 0000000000..eb7ccc36c3 --- /dev/null +++ b/library/Zend/Db/Sql/Predicate/In.php @@ -0,0 +1,133 @@ +setIdentifier($identifier); + } + if ($valueSet) { + $this->setValueSet($valueSet); + } + } + + /** + * Set identifier for comparison + * + * @param string|array $identifier + * @return In + */ + public function setIdentifier($identifier) + { + $this->identifier = $identifier; + + return $this; + } + + /** + * Get identifier of comparison + * + * @return null|string|array + */ + public function getIdentifier() + { + return $this->identifier; + } + + /** + * Set set of values for IN comparison + * + * @param array|Select $valueSet + * @throws Exception\InvalidArgumentException + * @return In + */ + public function setValueSet($valueSet) + { + if (!is_array($valueSet) && !$valueSet instanceof Select) { + throw new Exception\InvalidArgumentException( + '$valueSet must be either an array or a Zend\Db\Sql\Select object, ' . gettype($valueSet) . ' given' + ); + } + $this->valueSet = $valueSet; + + return $this; + } + + /** + * Gets set of values in IN comparision + * + * @return array|Select + */ + public function getValueSet() + { + return $this->valueSet; + } + + /** + * Return array of parts for where statement + * + * @return array + */ + public function getExpressionData() + { + $identifier = $this->getIdentifier(); + $values = $this->getValueSet(); + $replacements = array(); + + if (is_array($identifier)) { + $identifierSpecFragment = '(' . implode(', ', array_fill(0, count($identifier), '%s')) . ')'; + $types = array_fill(0, count($identifier), self::TYPE_IDENTIFIER); + $replacements = $identifier; + } else { + $identifierSpecFragment = '%s'; + $replacements[] = $identifier; + $types = array(self::TYPE_IDENTIFIER); + } + + if ($values instanceof Select) { + $specification = vsprintf( + $this->specification, + array($identifierSpecFragment, '%s') + ); + $replacements[] = $values; + $types[] = self::TYPE_VALUE; + } else { + $specification = vsprintf( + $this->specification, + array($identifierSpecFragment, '(' . implode(', ', array_fill(0, count($values), '%s')) . ')') + ); + $replacements = array_merge($replacements, $values); + $types = array_merge($types, array_fill(0, count($values), self::TYPE_VALUE)); + } + + return array(array( + $specification, + $replacements, + $types, + )); + } +} diff --git a/library/Zend/Db/Sql/Predicate/IsNotNull.php b/library/Zend/Db/Sql/Predicate/IsNotNull.php new file mode 100755 index 0000000000..e09f34912a --- /dev/null +++ b/library/Zend/Db/Sql/Predicate/IsNotNull.php @@ -0,0 +1,15 @@ +setIdentifier($identifier); + } + } + + /** + * Set identifier for comparison + * + * @param string $identifier + * @return IsNull + */ + public function setIdentifier($identifier) + { + $this->identifier = $identifier; + return $this; + } + + /** + * Get identifier of comparison + * + * @return null|string + */ + public function getIdentifier() + { + return $this->identifier; + } + + /** + * Set specification string to use in forming SQL predicate + * + * @param string $specification + * @return IsNull + */ + public function setSpecification($specification) + { + $this->specification = $specification; + return $this; + } + + /** + * Get specification string to use in forming SQL predicate + * + * @return string + */ + public function getSpecification() + { + return $this->specification; + } + + /** + * Get parts for where statement + * + * @return array + */ + public function getExpressionData() + { + return array(array( + $this->getSpecification(), + array($this->identifier), + array(self::TYPE_IDENTIFIER), + )); + } +} diff --git a/library/Zend/Db/Sql/Predicate/Like.php b/library/Zend/Db/Sql/Predicate/Like.php new file mode 100755 index 0000000000..b5d6676fe0 --- /dev/null +++ b/library/Zend/Db/Sql/Predicate/Like.php @@ -0,0 +1,106 @@ +setIdentifier($identifier); + } + if ($like) { + $this->setLike($like); + } + } + + /** + * @param string $identifier + * @return self + */ + public function setIdentifier($identifier) + { + $this->identifier = $identifier; + return $this; + } + + /** + * @return string + */ + public function getIdentifier() + { + return $this->identifier; + } + + /** + * @param string $like + * @return self + */ + public function setLike($like) + { + $this->like = $like; + return $this; + } + + /** + * @return string + */ + public function getLike() + { + return $this->like; + } + + /** + * @param string $specification + * @return self + */ + public function setSpecification($specification) + { + $this->specification = $specification; + return $this; + } + + /** + * @return string + */ + public function getSpecification() + { + return $this->specification; + } + + /** + * @return array + */ + public function getExpressionData() + { + return array( + array($this->specification, array($this->identifier, $this->like), array(self::TYPE_IDENTIFIER, self::TYPE_VALUE)) + ); + } +} diff --git a/library/Zend/Db/Sql/Predicate/Literal.php b/library/Zend/Db/Sql/Predicate/Literal.php new file mode 100755 index 0000000000..5ee68c998c --- /dev/null +++ b/library/Zend/Db/Sql/Predicate/Literal.php @@ -0,0 +1,16 @@ +'; + const OP_GT = '>'; + + const OPERATOR_GREATER_THAN_OR_EQUAL_TO = '>='; + const OP_GTE = '>='; + + protected $allowedTypes = array( + self::TYPE_IDENTIFIER, + self::TYPE_VALUE, + ); + + protected $left = null; + protected $leftType = self::TYPE_IDENTIFIER; + protected $operator = self::OPERATOR_EQUAL_TO; + protected $right = null; + protected $rightType = self::TYPE_VALUE; + + /** + * Constructor + * + * @param int|float|bool|string $left + * @param string $operator + * @param int|float|bool|string $right + * @param string $leftType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_IDENTIFIER {@see allowedTypes} + * @param string $rightType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_VALUE {@see allowedTypes} + */ + public function __construct($left = null, $operator = self::OPERATOR_EQUAL_TO, $right = null, $leftType = self::TYPE_IDENTIFIER, $rightType = self::TYPE_VALUE) + { + if ($left !== null) { + $this->setLeft($left); + } + + if ($operator !== self::OPERATOR_EQUAL_TO) { + $this->setOperator($operator); + } + + if ($right !== null) { + $this->setRight($right); + } + + if ($leftType !== self::TYPE_IDENTIFIER) { + $this->setLeftType($leftType); + } + + if ($rightType !== self::TYPE_VALUE) { + $this->setRightType($rightType); + } + } + + /** + * Set left side of operator + * + * @param int|float|bool|string $left + * @return Operator + */ + public function setLeft($left) + { + $this->left = $left; + return $this; + } + + /** + * Get left side of operator + * + * @return int|float|bool|string + */ + public function getLeft() + { + return $this->left; + } + + /** + * Set parameter type for left side of operator + * + * @param string $type TYPE_IDENTIFIER or TYPE_VALUE {@see allowedTypes} + * @throws Exception\InvalidArgumentException + * @return Operator + */ + public function setLeftType($type) + { + if (!in_array($type, $this->allowedTypes)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid type "%s" provided; must be of type "%s" or "%s"', + $type, + __CLASS__ . '::TYPE_IDENTIFIER', + __CLASS__ . '::TYPE_VALUE' + )); + } + $this->leftType = $type; + return $this; + } + + /** + * Get parameter type on left side of operator + * + * @return string + */ + public function getLeftType() + { + return $this->leftType; + } + + /** + * Set operator string + * + * @param string $operator + * @return Operator + */ + public function setOperator($operator) + { + $this->operator = $operator; + return $this; + } + + /** + * Get operator string + * + * @return string + */ + public function getOperator() + { + return $this->operator; + } + + /** + * Set right side of operator + * + * @param int|float|bool|string $value + * @return Operator + */ + public function setRight($value) + { + $this->right = $value; + return $this; + } + + /** + * Get right side of operator + * + * @return int|float|bool|string + */ + public function getRight() + { + return $this->right; + } + + /** + * Set parameter type for right side of operator + * + * @param string $type TYPE_IDENTIFIER or TYPE_VALUE {@see allowedTypes} + * @throws Exception\InvalidArgumentException + * @return Operator + */ + public function setRightType($type) + { + if (!in_array($type, $this->allowedTypes)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid type "%s" provided; must be of type "%s" or "%s"', + $type, + __CLASS__ . '::TYPE_IDENTIFIER', + __CLASS__ . '::TYPE_VALUE' + )); + } + $this->rightType = $type; + return $this; + } + + /** + * Get parameter type on right side of operator + * + * @return string + */ + public function getRightType() + { + return $this->rightType; + } + + /** + * Get predicate parts for where statement + * + * @return array + */ + public function getExpressionData() + { + return array(array( + '%s ' . $this->operator . ' %s', + array($this->left, $this->right), + array($this->leftType, $this->rightType) + )); + } +} diff --git a/library/Zend/Db/Sql/Predicate/Predicate.php b/library/Zend/Db/Sql/Predicate/Predicate.php new file mode 100755 index 0000000000..e9ef5757dc --- /dev/null +++ b/library/Zend/Db/Sql/Predicate/Predicate.php @@ -0,0 +1,409 @@ +setUnnest($this); + $this->addPredicate($predicateSet, ($this->nextPredicateCombineOperator) ?: $this->defaultCombination); + $this->nextPredicateCombineOperator = null; + return $predicateSet; + } + + /** + * Indicate what predicate will be unnested + * + * @param Predicate $predicate + * @return void + */ + public function setUnnest(Predicate $predicate) + { + $this->unnest = $predicate; + } + + /** + * Indicate end of nested predicate + * + * @return Predicate + * @throws RuntimeException + */ + public function unnest() + { + if ($this->unnest == null) { + throw new RuntimeException('Not nested'); + } + $unnset = $this->unnest; + $this->unnest = null; + return $unnset; + } + + /** + * Create "Equal To" predicate + * + * Utilizes Operator predicate + * + * @param int|float|bool|string $left + * @param int|float|bool|string $right + * @param string $leftType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_IDENTIFIER {@see allowedTypes} + * @param string $rightType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_VALUE {@see allowedTypes} + * @return Predicate + */ + public function equalTo($left, $right, $leftType = self::TYPE_IDENTIFIER, $rightType = self::TYPE_VALUE) + { + $this->addPredicate( + new Operator($left, Operator::OPERATOR_EQUAL_TO, $right, $leftType, $rightType), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "Not Equal To" predicate + * + * Utilizes Operator predicate + * + * @param int|float|bool|string $left + * @param int|float|bool|string $right + * @param string $leftType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_IDENTIFIER {@see allowedTypes} + * @param string $rightType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_VALUE {@see allowedTypes} + * @return Predicate + */ + public function notEqualTo($left, $right, $leftType = self::TYPE_IDENTIFIER, $rightType = self::TYPE_VALUE) + { + $this->addPredicate( + new Operator($left, Operator::OPERATOR_NOT_EQUAL_TO, $right, $leftType, $rightType), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "Less Than" predicate + * + * Utilizes Operator predicate + * + * @param int|float|bool|string $left + * @param int|float|bool|string $right + * @param string $leftType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_IDENTIFIER {@see allowedTypes} + * @param string $rightType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_VALUE {@see allowedTypes} + * @return Predicate + */ + public function lessThan($left, $right, $leftType = self::TYPE_IDENTIFIER, $rightType = self::TYPE_VALUE) + { + $this->addPredicate( + new Operator($left, Operator::OPERATOR_LESS_THAN, $right, $leftType, $rightType), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "Greater Than" predicate + * + * Utilizes Operator predicate + * + * @param int|float|bool|string $left + * @param int|float|bool|string $right + * @param string $leftType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_IDENTIFIER {@see allowedTypes} + * @param string $rightType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_VALUE {@see allowedTypes} + * @return Predicate + */ + public function greaterThan($left, $right, $leftType = self::TYPE_IDENTIFIER, $rightType = self::TYPE_VALUE) + { + $this->addPredicate( + new Operator($left, Operator::OPERATOR_GREATER_THAN, $right, $leftType, $rightType), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "Less Than Or Equal To" predicate + * + * Utilizes Operator predicate + * + * @param int|float|bool|string $left + * @param int|float|bool|string $right + * @param string $leftType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_IDENTIFIER {@see allowedTypes} + * @param string $rightType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_VALUE {@see allowedTypes} + * @return Predicate + */ + public function lessThanOrEqualTo($left, $right, $leftType = self::TYPE_IDENTIFIER, $rightType = self::TYPE_VALUE) + { + $this->addPredicate( + new Operator($left, Operator::OPERATOR_LESS_THAN_OR_EQUAL_TO, $right, $leftType, $rightType), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "Greater Than Or Equal To" predicate + * + * Utilizes Operator predicate + * + * @param int|float|bool|string $left + * @param int|float|bool|string $right + * @param string $leftType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_IDENTIFIER {@see allowedTypes} + * @param string $rightType TYPE_IDENTIFIER or TYPE_VALUE by default TYPE_VALUE {@see allowedTypes} + * @return Predicate + */ + public function greaterThanOrEqualTo($left, $right, $leftType = self::TYPE_IDENTIFIER, $rightType = self::TYPE_VALUE) + { + $this->addPredicate( + new Operator($left, Operator::OPERATOR_GREATER_THAN_OR_EQUAL_TO, $right, $leftType, $rightType), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "Like" predicate + * + * Utilizes Like predicate + * + * @param string $identifier + * @param string $like + * @return Predicate + */ + public function like($identifier, $like) + { + $this->addPredicate( + new Like($identifier, $like), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + /** + * Create "notLike" predicate + * + * Utilizes In predicate + * + * @param string $identifier + * @param string $notLike + * @return Predicate + */ + public function notLike($identifier, $notLike) + { + $this->addPredicate( + new NotLike($identifier, $notLike), + ($this->nextPredicateCombineOperator) ? : $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + return $this; + } + + /** + * Create an expression, with parameter placeholders + * + * @param $expression + * @param $parameters + * @return $this + */ + public function expression($expression, $parameters) + { + $this->addPredicate( + new Expression($expression, $parameters), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "Literal" predicate + * + * Literal predicate, for parameters, use expression() + * + * @param string $literal + * @return Predicate + */ + public function literal($literal) + { + // process deprecated parameters from previous literal($literal, $parameters = null) signature + if (func_num_args() >= 2) { + $parameters = func_get_arg(1); + $predicate = new Expression($literal, $parameters); + } + + // normal workflow for "Literals" here + if (!isset($predicate)) { + $predicate = new Literal($literal); + } + + $this->addPredicate( + $predicate, + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "IS NULL" predicate + * + * Utilizes IsNull predicate + * + * @param string $identifier + * @return Predicate + */ + public function isNull($identifier) + { + $this->addPredicate( + new IsNull($identifier), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "IS NOT NULL" predicate + * + * Utilizes IsNotNull predicate + * + * @param string $identifier + * @return Predicate + */ + public function isNotNull($identifier) + { + $this->addPredicate( + new IsNotNull($identifier), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "IN" predicate + * + * Utilizes In predicate + * + * @param string $identifier + * @param array|\Zend\Db\Sql\Select $valueSet + * @return Predicate + */ + public function in($identifier, $valueSet = null) + { + $this->addPredicate( + new In($identifier, $valueSet), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "NOT IN" predicate + * + * Utilizes NotIn predicate + * + * @param string $identifier + * @param array|\Zend\Db\Sql\Select $valueSet + * @return Predicate + */ + public function notIn($identifier, $valueSet = null) + { + $this->addPredicate( + new NotIn($identifier, $valueSet), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Create "between" predicate + * + * Utilizes Between predicate + * + * @param string $identifier + * @param int|float|string $minValue + * @param int|float|string $maxValue + * @return Predicate + */ + public function between($identifier, $minValue, $maxValue) + { + $this->addPredicate( + new Between($identifier, $minValue, $maxValue), + ($this->nextPredicateCombineOperator) ?: $this->defaultCombination + ); + $this->nextPredicateCombineOperator = null; + + return $this; + } + + /** + * Overloading + * + * Overloads "or", "and", "nest", and "unnest" + * + * @param string $name + * @return Predicate + */ + public function __get($name) + { + switch (strtolower($name)) { + case 'or': + $this->nextPredicateCombineOperator = self::OP_OR; + break; + case 'and': + $this->nextPredicateCombineOperator = self::OP_AND; + break; + case 'nest': + return $this->nest(); + case 'unnest': + return $this->unnest(); + } + return $this; + } +} diff --git a/library/Zend/Db/Sql/Predicate/PredicateInterface.php b/library/Zend/Db/Sql/Predicate/PredicateInterface.php new file mode 100755 index 0000000000..68b197f844 --- /dev/null +++ b/library/Zend/Db/Sql/Predicate/PredicateInterface.php @@ -0,0 +1,16 @@ +defaultCombination = $defaultCombination; + if ($predicates) { + foreach ($predicates as $predicate) { + $this->addPredicate($predicate); + } + } + } + + /** + * Add predicate to set + * + * @param PredicateInterface $predicate + * @param string $combination + * @return PredicateSet + */ + public function addPredicate(PredicateInterface $predicate, $combination = null) + { + if ($combination === null || !in_array($combination, array(self::OP_AND, self::OP_OR))) { + $combination = $this->defaultCombination; + } + + if ($combination == self::OP_OR) { + $this->orPredicate($predicate); + return $this; + } + + $this->andPredicate($predicate); + return $this; + } + + public function addPredicates($predicates, $combination = self::OP_AND) + { + if ($predicates === null) { + throw new Exception\InvalidArgumentException('Predicate cannot be null'); + } + if ($predicates instanceof PredicateInterface) { + $this->addPredicate($predicates, $combination); + return $this; + } + if ($predicates instanceof \Closure) { + $predicates($this); + return $this; + } + if (is_string($predicates)) { + // String $predicate should be passed as an expression + $predicates = (strpos($predicates, Expression::PLACEHOLDER) !== false) + ? new Expression($predicates) : new Literal($predicates); + $this->addPredicate($predicates, $combination); + return $this; + } + if (is_array($predicates)) { + foreach ($predicates as $pkey => $pvalue) { + // loop through predicates + if (is_string($pkey)) { + if (strpos($pkey, '?') !== false) { + // First, process strings that the abstraction replacement character ? + // as an Expression predicate + $predicates = new Expression($pkey, $pvalue); + } elseif ($pvalue === null) { // Otherwise, if still a string, do something intelligent with the PHP type provided + // map PHP null to SQL IS NULL expression + $predicates = new IsNull($pkey, $pvalue); + } elseif (is_array($pvalue)) { + // if the value is an array, assume IN() is desired + $predicates = new In($pkey, $pvalue); + } elseif ($pvalue instanceof PredicateInterface) { + throw new Exception\InvalidArgumentException( + 'Using Predicate must not use string keys' + ); + } else { + // otherwise assume that array('foo' => 'bar') means "foo" = 'bar' + $predicates = new Operator($pkey, Operator::OP_EQ, $pvalue); + } + } elseif ($pvalue instanceof PredicateInterface) { + // Predicate type is ok + $predicates = $pvalue; + } else { + // must be an array of expressions (with int-indexed array) + $predicates = (strpos($pvalue, Expression::PLACEHOLDER) !== false) + ? new Expression($pvalue) : new Literal($pvalue); + } + $this->addPredicate($predicates, $combination); + } + } + return $this; + } + + /** + * Return the predicates + * + * @return PredicateInterface[] + */ + public function getPredicates() + { + return $this->predicates; + } + + /** + * Add predicate using OR operator + * + * @param PredicateInterface $predicate + * @return PredicateSet + */ + public function orPredicate(PredicateInterface $predicate) + { + $this->predicates[] = array(self::OP_OR, $predicate); + return $this; + } + + /** + * Add predicate using AND operator + * + * @param PredicateInterface $predicate + * @return PredicateSet + */ + public function andPredicate(PredicateInterface $predicate) + { + $this->predicates[] = array(self::OP_AND, $predicate); + return $this; + } + + /** + * Get predicate parts for where statement + * + * @return array + */ + public function getExpressionData() + { + $parts = array(); + for ($i = 0, $count = count($this->predicates); $i < $count; $i++) { + /** @var $predicate PredicateInterface */ + $predicate = $this->predicates[$i][1]; + + if ($predicate instanceof PredicateSet) { + $parts[] = '('; + } + + $parts = array_merge($parts, $predicate->getExpressionData()); + + if ($predicate instanceof PredicateSet) { + $parts[] = ')'; + } + + if (isset($this->predicates[$i+1])) { + $parts[] = sprintf(' %s ', $this->predicates[$i+1][0]); + } + } + return $parts; + } + + /** + * Get count of attached predicates + * + * @return int + */ + public function count() + { + return count($this->predicates); + } +} diff --git a/library/Zend/Db/Sql/PreparableSqlInterface.php b/library/Zend/Db/Sql/PreparableSqlInterface.php new file mode 100755 index 0000000000..ea32cd65b7 --- /dev/null +++ b/library/Zend/Db/Sql/PreparableSqlInterface.php @@ -0,0 +1,23 @@ + '%1$s', + self::SELECT => array( + 'SELECT %1$s FROM %2$s' => array( + array(1 => '%1$s', 2 => '%1$s AS %2$s', 'combinedby' => ', '), + null + ), + 'SELECT %1$s %2$s FROM %3$s' => array( + null, + array(1 => '%1$s', 2 => '%1$s AS %2$s', 'combinedby' => ', '), + null + ), + 'SELECT %1$s' => array( + array(1 => '%1$s', 2 => '%1$s AS %2$s', 'combinedby' => ', '), + ), + ), + self::JOINS => array( + '%1$s' => array( + array(3 => '%1$s JOIN %2$s ON %3$s', 'combinedby' => ' ') + ) + ), + self::WHERE => 'WHERE %1$s', + self::GROUP => array( + 'GROUP BY %1$s' => array( + array(1 => '%1$s', 'combinedby' => ', ') + ) + ), + self::HAVING => 'HAVING %1$s', + self::ORDER => array( + 'ORDER BY %1$s' => array( + array(1 => '%1$s', 2 => '%1$s %2$s', 'combinedby' => ', ') + ) + ), + self::LIMIT => 'LIMIT %1$s', + self::OFFSET => 'OFFSET %1$s', + 'statementEnd' => '%1$s', + self::COMBINE => '%1$s ( %2$s )', + ); + + /** + * @var bool + */ + protected $tableReadOnly = false; + + /** + * @var bool + */ + protected $prefixColumnsWithTable = true; + + /** + * @var string|array|TableIdentifier + */ + protected $table = null; + + /** + * @var null|string|Expression + */ + protected $quantifier = null; + + /** + * @var array + */ + protected $columns = array(self::SQL_STAR); + + /** + * @var array + */ + protected $joins = array(); + + /** + * @var Where + */ + protected $where = null; + + /** + * @var array + */ + protected $order = array(); + + /** + * @var null|array + */ + protected $group = null; + + /** + * @var null|string|array + */ + protected $having = null; + + /** + * @var int|null + */ + protected $limit = null; + + /** + * @var int|null + */ + protected $offset = null; + + /** + * @var array + */ + protected $combine = array(); + + /** + * Constructor + * + * @param null|string|array|TableIdentifier $table + */ + public function __construct($table = null) + { + if ($table) { + $this->from($table); + $this->tableReadOnly = true; + } + + $this->where = new Where; + $this->having = new Having; + } + + /** + * Create from clause + * + * @param string|array|TableIdentifier $table + * @throws Exception\InvalidArgumentException + * @return Select + */ + public function from($table) + { + if ($this->tableReadOnly) { + throw new Exception\InvalidArgumentException('Since this object was created with a table and/or schema in the constructor, it is read only.'); + } + + if (!is_string($table) && !is_array($table) && !$table instanceof TableIdentifier) { + throw new Exception\InvalidArgumentException('$table must be a string, array, or an instance of TableIdentifier'); + } + + if (is_array($table) && (!is_string(key($table)) || count($table) !== 1)) { + throw new Exception\InvalidArgumentException('from() expects $table as an array is a single element associative array'); + } + + $this->table = $table; + return $this; + } + + /** + * @param string|Expression $quantifier DISTINCT|ALL + * @return Select + */ + public function quantifier($quantifier) + { + if (!is_string($quantifier) && !$quantifier instanceof Expression) { + throw new Exception\InvalidArgumentException( + 'Quantifier must be one of DISTINCT, ALL, or some platform specific Expression object' + ); + } + $this->quantifier = $quantifier; + return $this; + } + + /** + * Specify columns from which to select + * + * Possible valid states: + * + * array(*) + * + * array(value, ...) + * value can be strings or Expression objects + * + * array(string => value, ...) + * key string will be use as alias, + * value can be string or Expression objects + * + * @param array $columns + * @param bool $prefixColumnsWithTable + * @return Select + */ + public function columns(array $columns, $prefixColumnsWithTable = true) + { + $this->columns = $columns; + $this->prefixColumnsWithTable = (bool) $prefixColumnsWithTable; + return $this; + } + + /** + * Create join clause + * + * @param string|array $name + * @param string $on + * @param string|array $columns + * @param string $type one of the JOIN_* constants + * @throws Exception\InvalidArgumentException + * @return Select + */ + public function join($name, $on, $columns = self::SQL_STAR, $type = self::JOIN_INNER) + { + if (is_array($name) && (!is_string(key($name)) || count($name) !== 1)) { + throw new Exception\InvalidArgumentException( + sprintf("join() expects '%s' as an array is a single element associative array", array_shift($name)) + ); + } + if (!is_array($columns)) { + $columns = array($columns); + } + $this->joins[] = array( + 'name' => $name, + 'on' => $on, + 'columns' => $columns, + 'type' => $type + ); + return $this; + } + + /** + * Create where clause + * + * @param Where|\Closure|string|array|Predicate\PredicateInterface $predicate + * @param string $combination One of the OP_* constants from Predicate\PredicateSet + * @throws Exception\InvalidArgumentException + * @return Select + */ + public function where($predicate, $combination = Predicate\PredicateSet::OP_AND) + { + if ($predicate instanceof Where) { + $this->where = $predicate; + } else { + $this->where->addPredicates($predicate, $combination); + } + return $this; + } + + public function group($group) + { + if (is_array($group)) { + foreach ($group as $o) { + $this->group[] = $o; + } + } else { + $this->group[] = $group; + } + return $this; + } + + /** + * Create where clause + * + * @param Where|\Closure|string|array $predicate + * @param string $combination One of the OP_* constants from Predicate\PredicateSet + * @return Select + */ + public function having($predicate, $combination = Predicate\PredicateSet::OP_AND) + { + if ($predicate instanceof Having) { + $this->having = $predicate; + } else { + $this->having->addPredicates($predicate, $combination); + } + return $this; + } + + /** + * @param string|array $order + * @return Select + */ + public function order($order) + { + if (is_string($order)) { + if (strpos($order, ',') !== false) { + $order = preg_split('#,\s+#', $order); + } else { + $order = (array) $order; + } + } elseif (!is_array($order)) { + $order = array($order); + } + foreach ($order as $k => $v) { + if (is_string($k)) { + $this->order[$k] = $v; + } else { + $this->order[] = $v; + } + } + return $this; + } + + /** + * @param int $limit + * @return Select + */ + public function limit($limit) + { + if (!is_numeric($limit)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects parameter to be numeric, "%s" given', + __METHOD__, + (is_object($limit) ? get_class($limit) : gettype($limit)) + )); + } + + $this->limit = $limit; + return $this; + } + + /** + * @param int $offset + * @return Select + */ + public function offset($offset) + { + if (!is_numeric($offset)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects parameter to be numeric, "%s" given', + __METHOD__, + (is_object($offset) ? get_class($offset) : gettype($offset)) + )); + } + + $this->offset = $offset; + return $this; + } + + /** + * @param Select $select + * @param string $type + * @param string $modifier + * @return Select + * @throws Exception\InvalidArgumentException + */ + public function combine(Select $select, $type = self::COMBINE_UNION, $modifier = '') + { + if ($this->combine !== array()) { + throw new Exception\InvalidArgumentException('This Select object is already combined and cannot be combined with multiple Selects objects'); + } + $this->combine = array( + 'select' => $select, + 'type' => $type, + 'modifier' => $modifier + ); + return $this; + } + + /** + * @param string $part + * @return Select + * @throws Exception\InvalidArgumentException + */ + public function reset($part) + { + switch ($part) { + case self::TABLE: + if ($this->tableReadOnly) { + throw new Exception\InvalidArgumentException( + 'Since this object was created with a table and/or schema in the constructor, it is read only.' + ); + } + $this->table = null; + break; + case self::QUANTIFIER: + $this->quantifier = null; + break; + case self::COLUMNS: + $this->columns = array(); + break; + case self::JOINS: + $this->joins = array(); + break; + case self::WHERE: + $this->where = new Where; + break; + case self::GROUP: + $this->group = null; + break; + case self::HAVING: + $this->having = new Having; + break; + case self::LIMIT: + $this->limit = null; + break; + case self::OFFSET: + $this->offset = null; + break; + case self::ORDER: + $this->order = array(); + break; + case self::COMBINE: + $this->combine = array(); + break; + } + return $this; + } + + public function setSpecification($index, $specification) + { + if (!method_exists($this, 'process' . $index)) { + throw new Exception\InvalidArgumentException('Not a valid specification name.'); + } + $this->specifications[$index] = $specification; + return $this; + } + + public function getRawState($key = null) + { + $rawState = array( + self::TABLE => $this->table, + self::QUANTIFIER => $this->quantifier, + self::COLUMNS => $this->columns, + self::JOINS => $this->joins, + self::WHERE => $this->where, + self::ORDER => $this->order, + self::GROUP => $this->group, + self::HAVING => $this->having, + self::LIMIT => $this->limit, + self::OFFSET => $this->offset, + self::COMBINE => $this->combine + ); + return (isset($key) && array_key_exists($key, $rawState)) ? $rawState[$key] : $rawState; + } + + /** + * Prepare statement + * + * @param AdapterInterface $adapter + * @param StatementContainerInterface $statementContainer + * @return void + */ + public function prepareStatement(AdapterInterface $adapter, StatementContainerInterface $statementContainer) + { + // ensure statement has a ParameterContainer + $parameterContainer = $statementContainer->getParameterContainer(); + if (!$parameterContainer instanceof ParameterContainer) { + $parameterContainer = new ParameterContainer(); + $statementContainer->setParameterContainer($parameterContainer); + } + + $sqls = array(); + $parameters = array(); + $platform = $adapter->getPlatform(); + $driver = $adapter->getDriver(); + + foreach ($this->specifications as $name => $specification) { + $parameters[$name] = $this->{'process' . $name}($platform, $driver, $parameterContainer, $sqls, $parameters); + if ($specification && is_array($parameters[$name])) { + $sqls[$name] = $this->createSqlFromSpecificationAndParameters($specification, $parameters[$name]); + } + } + + $sql = implode(' ', $sqls); + + $statementContainer->setSql($sql); + return; + } + + /** + * Get SQL string for statement + * + * @param null|PlatformInterface $adapterPlatform If null, defaults to Sql92 + * @return string + */ + public function getSqlString(PlatformInterface $adapterPlatform = null) + { + // get platform, or create default + $adapterPlatform = ($adapterPlatform) ?: new AdapterSql92Platform; + + $sqls = array(); + $parameters = array(); + + foreach ($this->specifications as $name => $specification) { + $parameters[$name] = $this->{'process' . $name}($adapterPlatform, null, null, $sqls, $parameters); + if ($specification && is_array($parameters[$name])) { + $sqls[$name] = $this->createSqlFromSpecificationAndParameters($specification, $parameters[$name]); + } + } + + $sql = implode(' ', $sqls); + return $sql; + } + + /** + * Returns whether the table is read only or not. + * + * @return bool + */ + public function isTableReadOnly() + { + return $this->tableReadOnly; + } + + /** + * Render table with alias in from/join parts + * + * @todo move TableIdentifier concatination here + * @param string $table + * @param string $alias + * @return string + */ + protected function renderTable($table, $alias = null) + { + $sql = $table; + if ($alias) { + $sql .= ' AS ' . $alias; + } + return $sql; + } + + protected function processStatementStart(PlatformInterface $platform, DriverInterface $driver = null, ParameterContainer $parameterContainer = null) + { + if ($this->combine !== array()) { + return array('('); + } + } + + protected function processStatementEnd(PlatformInterface $platform, DriverInterface $driver = null, ParameterContainer $parameterContainer = null) + { + if ($this->combine !== array()) { + return array(')'); + } + } + + /** + * Process the select part + * + * @param PlatformInterface $platform + * @param DriverInterface $driver + * @param ParameterContainer $parameterContainer + * @return null|array + */ + protected function processSelect(PlatformInterface $platform, DriverInterface $driver = null, ParameterContainer $parameterContainer = null) + { + $expr = 1; + + if ($this->table) { + $table = $this->table; + $schema = $alias = null; + + if (is_array($table)) { + $alias = key($this->table); + $table = current($this->table); + } + + // create quoted table name to use in columns processing + if ($table instanceof TableIdentifier) { + list($table, $schema) = $table->getTableAndSchema(); + } + + if ($table instanceof Select) { + $table = '(' . $this->processSubselect($table, $platform, $driver, $parameterContainer) . ')'; + } else { + $table = $platform->quoteIdentifier($table); + } + + if ($schema) { + $table = $platform->quoteIdentifier($schema) . $platform->getIdentifierSeparator() . $table; + } + + if ($alias) { + $fromTable = $platform->quoteIdentifier($alias); + $table = $this->renderTable($table, $fromTable); + } else { + $fromTable = $table; + } + } else { + $fromTable = ''; + } + + if ($this->prefixColumnsWithTable) { + $fromTable .= $platform->getIdentifierSeparator(); + } else { + $fromTable = ''; + } + + // process table columns + $columns = array(); + foreach ($this->columns as $columnIndexOrAs => $column) { + $columnName = ''; + if ($column === self::SQL_STAR) { + $columns[] = array($fromTable . self::SQL_STAR); + continue; + } + + if ($column instanceof ExpressionInterface) { + $columnParts = $this->processExpression( + $column, + $platform, + $driver, + $this->processInfo['paramPrefix'] . ((is_string($columnIndexOrAs)) ? $columnIndexOrAs : 'column') + ); + if ($parameterContainer) { + $parameterContainer->merge($columnParts->getParameterContainer()); + } + $columnName .= $columnParts->getSql(); + } else { + $columnName .= $fromTable . $platform->quoteIdentifier($column); + } + + // process As portion + if (is_string($columnIndexOrAs)) { + $columnAs = $platform->quoteIdentifier($columnIndexOrAs); + } elseif (stripos($columnName, ' as ') === false) { + $columnAs = (is_string($column)) ? $platform->quoteIdentifier($column) : 'Expression' . $expr++; + } + $columns[] = (isset($columnAs)) ? array($columnName, $columnAs) : array($columnName); + } + + $separator = $platform->getIdentifierSeparator(); + + // process join columns + foreach ($this->joins as $join) { + foreach ($join['columns'] as $jKey => $jColumn) { + $jColumns = array(); + if ($jColumn instanceof ExpressionInterface) { + $jColumnParts = $this->processExpression( + $jColumn, + $platform, + $driver, + $this->processInfo['paramPrefix'] . ((is_string($jKey)) ? $jKey : 'column') + ); + if ($parameterContainer) { + $parameterContainer->merge($jColumnParts->getParameterContainer()); + } + $jColumns[] = $jColumnParts->getSql(); + } else { + $name = (is_array($join['name'])) ? key($join['name']) : $name = $join['name']; + if ($name instanceof TableIdentifier) { + $name = ($name->hasSchema() ? $platform->quoteIdentifier($name->getSchema()) . $separator : '') . $platform->quoteIdentifier($name->getTable()); + } else { + $name = $platform->quoteIdentifier($name); + } + $jColumns[] = $name . $separator . $platform->quoteIdentifierInFragment($jColumn); + } + if (is_string($jKey)) { + $jColumns[] = $platform->quoteIdentifier($jKey); + } elseif ($jColumn !== self::SQL_STAR) { + $jColumns[] = $platform->quoteIdentifier($jColumn); + } + $columns[] = $jColumns; + } + } + + if ($this->quantifier) { + if ($this->quantifier instanceof ExpressionInterface) { + $quantifierParts = $this->processExpression($this->quantifier, $platform, $driver, 'quantifier'); + if ($parameterContainer) { + $parameterContainer->merge($quantifierParts->getParameterContainer()); + } + $quantifier = $quantifierParts->getSql(); + } else { + $quantifier = $this->quantifier; + } + } + + if (!isset($table)) { + return array($columns); + } elseif (isset($quantifier)) { + return array($quantifier, $columns, $table); + } else { + return array($columns, $table); + } + } + + protected function processJoins(PlatformInterface $platform, DriverInterface $driver = null, ParameterContainer $parameterContainer = null) + { + if (!$this->joins) { + return null; + } + + // process joins + $joinSpecArgArray = array(); + foreach ($this->joins as $j => $join) { + $joinSpecArgArray[$j] = array(); + $joinName = null; + $joinAs = null; + + // type + $joinSpecArgArray[$j][] = strtoupper($join['type']); + + // table name + if (is_array($join['name'])) { + $joinName = current($join['name']); + $joinAs = $platform->quoteIdentifier(key($join['name'])); + } else { + $joinName = $join['name']; + } + if ($joinName instanceof ExpressionInterface) { + $joinName = $joinName->getExpression(); + } elseif ($joinName instanceof TableIdentifier) { + $joinName = $joinName->getTableAndSchema(); + $joinName = ($joinName[1] ? $platform->quoteIdentifier($joinName[1]) . $platform->getIdentifierSeparator() : '') . $platform->quoteIdentifier($joinName[0]); + } else { + if ($joinName instanceof Select) { + $joinName = '(' . $this->processSubSelect($joinName, $platform, $driver, $parameterContainer) . ')'; + } else { + $joinName = $platform->quoteIdentifier($joinName); + } + } + $joinSpecArgArray[$j][] = (isset($joinAs)) ? $joinName . ' AS ' . $joinAs : $joinName; + + // on expression + // note: for Expression objects, pass them to processExpression with a prefix specific to each join (used for named parameters) + $joinSpecArgArray[$j][] = ($join['on'] instanceof ExpressionInterface) + ? $this->processExpression($join['on'], $platform, $driver, $this->processInfo['paramPrefix'] . 'join' . ($j+1) . 'part') + : $platform->quoteIdentifierInFragment($join['on'], array('=', 'AND', 'OR', '(', ')', 'BETWEEN', '<', '>')); // on + if ($joinSpecArgArray[$j][2] instanceof StatementContainerInterface) { + if ($parameterContainer) { + $parameterContainer->merge($joinSpecArgArray[$j][2]->getParameterContainer()); + } + $joinSpecArgArray[$j][2] = $joinSpecArgArray[$j][2]->getSql(); + } + } + + return array($joinSpecArgArray); + } + + protected function processWhere(PlatformInterface $platform, DriverInterface $driver = null, ParameterContainer $parameterContainer = null) + { + if ($this->where->count() == 0) { + return null; + } + $whereParts = $this->processExpression($this->where, $platform, $driver, $this->processInfo['paramPrefix'] . 'where'); + if ($parameterContainer) { + $parameterContainer->merge($whereParts->getParameterContainer()); + } + return array($whereParts->getSql()); + } + + protected function processGroup(PlatformInterface $platform, DriverInterface $driver = null, ParameterContainer $parameterContainer = null) + { + if ($this->group === null) { + return null; + } + // process table columns + $groups = array(); + foreach ($this->group as $column) { + $columnSql = ''; + if ($column instanceof Expression) { + $columnParts = $this->processExpression($column, $platform, $driver, $this->processInfo['paramPrefix'] . 'group'); + if ($parameterContainer) { + $parameterContainer->merge($columnParts->getParameterContainer()); + } + $columnSql .= $columnParts->getSql(); + } else { + $columnSql .= $platform->quoteIdentifierInFragment($column); + } + $groups[] = $columnSql; + } + return array($groups); + } + + protected function processHaving(PlatformInterface $platform, DriverInterface $driver = null, ParameterContainer $parameterContainer = null) + { + if ($this->having->count() == 0) { + return null; + } + $whereParts = $this->processExpression($this->having, $platform, $driver, $this->processInfo['paramPrefix'] . 'having'); + if ($parameterContainer) { + $parameterContainer->merge($whereParts->getParameterContainer()); + } + return array($whereParts->getSql()); + } + + protected function processOrder(PlatformInterface $platform, DriverInterface $driver = null, ParameterContainer $parameterContainer = null) + { + if (empty($this->order)) { + return null; + } + $orders = array(); + foreach ($this->order as $k => $v) { + if ($v instanceof Expression) { + /** @var $orderParts \Zend\Db\Adapter\StatementContainer */ + $orderParts = $this->processExpression($v, $platform, $driver); + if ($parameterContainer) { + $parameterContainer->merge($orderParts->getParameterContainer()); + } + $orders[] = array($orderParts->getSql()); + continue; + } + if (is_int($k)) { + if (strpos($v, ' ') !== false) { + list($k, $v) = preg_split('# #', $v, 2); + } else { + $k = $v; + $v = self::ORDER_ASCENDING; + } + } + if (strtoupper($v) == self::ORDER_DESCENDING) { + $orders[] = array($platform->quoteIdentifierInFragment($k), self::ORDER_DESCENDING); + } else { + $orders[] = array($platform->quoteIdentifierInFragment($k), self::ORDER_ASCENDING); + } + } + return array($orders); + } + + protected function processLimit(PlatformInterface $platform, DriverInterface $driver = null, ParameterContainer $parameterContainer = null) + { + if ($this->limit === null) { + return null; + } + + $limit = $this->limit; + + if ($driver) { + $sql = $driver->formatParameterName('limit'); + $parameterContainer->offsetSet('limit', $limit, ParameterContainer::TYPE_INTEGER); + } else { + $sql = $platform->quoteValue($limit); + } + + return array($sql); + } + + protected function processOffset(PlatformInterface $platform, DriverInterface $driver = null, ParameterContainer $parameterContainer = null) + { + if ($this->offset === null) { + return null; + } + + $offset = $this->offset; + + if ($driver) { + $parameterContainer->offsetSet('offset', $offset, ParameterContainer::TYPE_INTEGER); + return array($driver->formatParameterName('offset')); + } + + return array($platform->quoteValue($offset)); + } + + protected function processCombine(PlatformInterface $platform, DriverInterface $driver = null, ParameterContainer $parameterContainer = null) + { + if ($this->combine == array()) { + return null; + } + + $type = $this->combine['type']; + if ($this->combine['modifier']) { + $type .= ' ' . $this->combine['modifier']; + } + $type = strtoupper($type); + + if ($driver) { + $sql = $this->processSubSelect($this->combine['select'], $platform, $driver, $parameterContainer); + return array($type, $sql); + } + return array( + $type, + $this->processSubSelect($this->combine['select'], $platform) + ); + } + + /** + * Variable overloading + * + * @param string $name + * @throws Exception\InvalidArgumentException + * @return mixed + */ + public function __get($name) + { + switch (strtolower($name)) { + case 'where': + return $this->where; + case 'having': + return $this->having; + default: + throw new Exception\InvalidArgumentException('Not a valid magic property for this object'); + } + } + + /** + * __clone + * + * Resets the where object each time the Select is cloned. + * + * @return void + */ + public function __clone() + { + $this->where = clone $this->where; + $this->having = clone $this->having; + } +} diff --git a/library/Zend/Db/Sql/Sql.php b/library/Zend/Db/Sql/Sql.php new file mode 100755 index 0000000000..17a697e2d8 --- /dev/null +++ b/library/Zend/Db/Sql/Sql.php @@ -0,0 +1,151 @@ +adapter = $adapter; + if ($table) { + $this->setTable($table); + } + $this->sqlPlatform = ($sqlPlatform) ?: new Platform\Platform($adapter); + } + + /** + * @return null|\Zend\Db\Adapter\AdapterInterface + */ + public function getAdapter() + { + return $this->adapter; + } + + public function hasTable() + { + return ($this->table != null); + } + + public function setTable($table) + { + if (is_string($table) || is_array($table) || $table instanceof TableIdentifier) { + $this->table = $table; + } else { + throw new Exception\InvalidArgumentException('Table must be a string, array or instance of TableIdentifier.'); + } + return $this; + } + + public function getTable() + { + return $this->table; + } + + public function getSqlPlatform() + { + return $this->sqlPlatform; + } + + public function select($table = null) + { + if ($this->table !== null && $table !== null) { + throw new Exception\InvalidArgumentException(sprintf( + 'This Sql object is intended to work with only the table "%s" provided at construction time.', + $this->table + )); + } + return new Select(($table) ?: $this->table); + } + + public function insert($table = null) + { + if ($this->table !== null && $table !== null) { + throw new Exception\InvalidArgumentException(sprintf( + 'This Sql object is intended to work with only the table "%s" provided at construction time.', + $this->table + )); + } + return new Insert(($table) ?: $this->table); + } + + public function update($table = null) + { + if ($this->table !== null && $table !== null) { + throw new Exception\InvalidArgumentException(sprintf( + 'This Sql object is intended to work with only the table "%s" provided at construction time.', + $this->table + )); + } + return new Update(($table) ?: $this->table); + } + + public function delete($table = null) + { + if ($this->table !== null && $table !== null) { + throw new Exception\InvalidArgumentException(sprintf( + 'This Sql object is intended to work with only the table "%s" provided at construction time.', + $this->table + )); + } + return new Delete(($table) ?: $this->table); + } + + /** + * @param PreparableSqlInterface $sqlObject + * @param StatementInterface|null $statement + * @return StatementInterface + */ + public function prepareStatementForSqlObject(PreparableSqlInterface $sqlObject, StatementInterface $statement = null) + { + $statement = ($statement) ?: $this->adapter->getDriver()->createStatement(); + + if ($this->sqlPlatform) { + $this->sqlPlatform->setSubject($sqlObject); + $this->sqlPlatform->prepareStatement($this->adapter, $statement); + } else { + $sqlObject->prepareStatement($this->adapter, $statement); + } + + return $statement; + } + + /** + * Get sql string using platform or sql object + * + * @param SqlInterface $sqlObject + * @param PlatformInterface $platform + * + * @return string + */ + public function getSqlStringForSqlObject(SqlInterface $sqlObject, PlatformInterface $platform = null) + { + $platform = ($platform) ?: $this->adapter->getPlatform(); + + if ($this->sqlPlatform) { + $this->sqlPlatform->setSubject($sqlObject); + return $this->sqlPlatform->getSqlString($platform); + } + + return $sqlObject->getSqlString($platform); + } +} diff --git a/library/Zend/Db/Sql/SqlInterface.php b/library/Zend/Db/Sql/SqlInterface.php new file mode 100755 index 0000000000..f7db64c17c --- /dev/null +++ b/library/Zend/Db/Sql/SqlInterface.php @@ -0,0 +1,22 @@ +table = $table; + $this->schema = $schema; + } + + /** + * @param string $table + */ + public function setTable($table) + { + $this->table = $table; + } + + /** + * @return string + */ + public function getTable() + { + return $this->table; + } + + /** + * @return bool + */ + public function hasSchema() + { + return ($this->schema != null); + } + + /** + * @param $schema + */ + public function setSchema($schema) + { + $this->schema = $schema; + } + + /** + * @return null|string + */ + public function getSchema() + { + return $this->schema; + } + + public function getTableAndSchema() + { + return array($this->table, $this->schema); + } +} diff --git a/library/Zend/Db/Sql/Update.php b/library/Zend/Db/Sql/Update.php new file mode 100755 index 0000000000..11e44e8340 --- /dev/null +++ b/library/Zend/Db/Sql/Update.php @@ -0,0 +1,271 @@ + 'UPDATE %1$s SET %2$s', + self::SPECIFICATION_WHERE => 'WHERE %1$s' + ); + + /** + * @var string|TableIdentifier + */ + protected $table = ''; + + /** + * @var bool + */ + protected $emptyWhereProtection = true; + + /** + * @var PriorityList + */ + protected $set; + + /** + * @var string|Where + */ + protected $where = null; + + /** + * Constructor + * + * @param null|string|TableIdentifier $table + */ + public function __construct($table = null) + { + if ($table) { + $this->table($table); + } + $this->where = new Where(); + $this->set = new PriorityList(); + $this->set->isLIFO(false); + } + + /** + * Specify table for statement + * + * @param string|TableIdentifier $table + * @return Update + */ + public function table($table) + { + $this->table = $table; + return $this; + } + + /** + * Set key/value pairs to update + * + * @param array $values Associative array of key values + * @param string $flag One of the VALUES_* constants + * @throws Exception\InvalidArgumentException + * @return Update + */ + public function set(array $values, $flag = self::VALUES_SET) + { + if ($values == null) { + throw new Exception\InvalidArgumentException('set() expects an array of values'); + } + + if ($flag == self::VALUES_SET) { + $this->set->clear(); + } + $priority = is_numeric($flag) ? $flag : 0; + foreach ($values as $k => $v) { + if (!is_string($k)) { + throw new Exception\InvalidArgumentException('set() expects a string for the value key'); + } + $this->set->insert($k, $v, $priority); + } + return $this; + } + + /** + * Create where clause + * + * @param Where|\Closure|string|array $predicate + * @param string $combination One of the OP_* constants from Predicate\PredicateSet + * @throws Exception\InvalidArgumentException + * @return Select + */ + public function where($predicate, $combination = Predicate\PredicateSet::OP_AND) + { + if ($predicate instanceof Where) { + $this->where = $predicate; + } else { + $this->where->addPredicates($predicate, $combination); + } + return $this; + } + + public function getRawState($key = null) + { + $rawState = array( + 'emptyWhereProtection' => $this->emptyWhereProtection, + 'table' => $this->table, + 'set' => $this->set->toArray(), + 'where' => $this->where + ); + return (isset($key) && array_key_exists($key, $rawState)) ? $rawState[$key] : $rawState; + } + + /** + * Prepare statement + * + * @param AdapterInterface $adapter + * @param StatementContainerInterface $statementContainer + * @return void + */ + public function prepareStatement(AdapterInterface $adapter, StatementContainerInterface $statementContainer) + { + $driver = $adapter->getDriver(); + $platform = $adapter->getPlatform(); + $parameterContainer = $statementContainer->getParameterContainer(); + + if (!$parameterContainer instanceof ParameterContainer) { + $parameterContainer = new ParameterContainer(); + $statementContainer->setParameterContainer($parameterContainer); + } + + $table = $this->table; + $schema = null; + + // create quoted table name to use in update processing + if ($table instanceof TableIdentifier) { + list($table, $schema) = $table->getTableAndSchema(); + } + + $table = $platform->quoteIdentifier($table); + + if ($schema) { + $table = $platform->quoteIdentifier($schema) . $platform->getIdentifierSeparator() . $table; + } + + $setSql = array(); + foreach ($this->set as $column => $value) { + if ($value instanceof Expression) { + $exprData = $this->processExpression($value, $platform, $driver); + $setSql[] = $platform->quoteIdentifier($column) . ' = ' . $exprData->getSql(); + $parameterContainer->merge($exprData->getParameterContainer()); + } else { + $setSql[] = $platform->quoteIdentifier($column) . ' = ' . $driver->formatParameterName($column); + $parameterContainer->offsetSet($column, $value); + } + } + $set = implode(', ', $setSql); + + $sql = sprintf($this->specifications[static::SPECIFICATION_UPDATE], $table, $set); + + // process where + if ($this->where->count() > 0) { + $whereParts = $this->processExpression($this->where, $platform, $driver, 'where'); + $parameterContainer->merge($whereParts->getParameterContainer()); + $sql .= ' ' . sprintf($this->specifications[static::SPECIFICATION_WHERE], $whereParts->getSql()); + } + $statementContainer->setSql($sql); + } + + /** + * Get SQL string for statement + * + * @param null|PlatformInterface $adapterPlatform If null, defaults to Sql92 + * @return string + */ + public function getSqlString(PlatformInterface $adapterPlatform = null) + { + $adapterPlatform = ($adapterPlatform) ?: new Sql92; + $table = $this->table; + $schema = null; + + // create quoted table name to use in update processing + if ($table instanceof TableIdentifier) { + list($table, $schema) = $table->getTableAndSchema(); + } + + $table = $adapterPlatform->quoteIdentifier($table); + + if ($schema) { + $table = $adapterPlatform->quoteIdentifier($schema) . $adapterPlatform->getIdentifierSeparator() . $table; + } + + $setSql = array(); + foreach ($this->set as $column => $value) { + if ($value instanceof ExpressionInterface) { + $exprData = $this->processExpression($value, $adapterPlatform); + $setSql[] = $adapterPlatform->quoteIdentifier($column) . ' = ' . $exprData->getSql(); + } elseif ($value === null) { + $setSql[] = $adapterPlatform->quoteIdentifier($column) . ' = NULL'; + } else { + $setSql[] = $adapterPlatform->quoteIdentifier($column) . ' = ' . $adapterPlatform->quoteValue($value); + } + } + $set = implode(', ', $setSql); + + $sql = sprintf($this->specifications[static::SPECIFICATION_UPDATE], $table, $set); + if ($this->where->count() > 0) { + $whereParts = $this->processExpression($this->where, $adapterPlatform, null, 'where'); + $sql .= ' ' . sprintf($this->specifications[static::SPECIFICATION_WHERE], $whereParts->getSql()); + } + return $sql; + } + + /** + * Variable overloading + * + * Proxies to "where" only + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + switch (strtolower($name)) { + case 'where': + return $this->where; + } + } + + /** + * __clone + * + * Resets the where object each time the Update is cloned. + * + * @return void + */ + public function __clone() + { + $this->where = clone $this->where; + $this->set = clone $this->set; + } +} diff --git a/library/Zend/Db/Sql/Where.php b/library/Zend/Db/Sql/Where.php new file mode 100755 index 0000000000..f50efb46f2 --- /dev/null +++ b/library/Zend/Db/Sql/Where.php @@ -0,0 +1,14 @@ +isInitialized; + } + + /** + * Initialize + * + * @throws Exception\RuntimeException + * @return null + */ + public function initialize() + { + if ($this->isInitialized) { + return; + } + + if (!$this->featureSet instanceof Feature\FeatureSet) { + $this->featureSet = new Feature\FeatureSet; + } + + $this->featureSet->setTableGateway($this); + $this->featureSet->apply('preInitialize', array()); + + if (!$this->adapter instanceof AdapterInterface) { + throw new Exception\RuntimeException('This table does not have an Adapter setup'); + } + + if (!is_string($this->table) && !$this->table instanceof TableIdentifier) { + throw new Exception\RuntimeException('This table object does not have a valid table set.'); + } + + if (!$this->resultSetPrototype instanceof ResultSetInterface) { + $this->resultSetPrototype = new ResultSet; + } + + if (!$this->sql instanceof Sql) { + $this->sql = new Sql($this->adapter, $this->table); + } + + $this->featureSet->apply('postInitialize', array()); + + $this->isInitialized = true; + } + + /** + * Get table name + * + * @return string + */ + public function getTable() + { + return $this->table; + } + + /** + * Get adapter + * + * @return AdapterInterface + */ + public function getAdapter() + { + return $this->adapter; + } + + /** + * @return array + */ + public function getColumns() + { + return $this->columns; + } + + /** + * @return Feature\FeatureSet + */ + public function getFeatureSet() + { + return $this->featureSet; + } + + /** + * Get select result prototype + * + * @return ResultSet + */ + public function getResultSetPrototype() + { + return $this->resultSetPrototype; + } + + /** + * @return Sql + */ + public function getSql() + { + return $this->sql; + } + + /** + * Select + * + * @param Where|\Closure|string|array $where + * @return ResultSet + */ + public function select($where = null) + { + if (!$this->isInitialized) { + $this->initialize(); + } + + $select = $this->sql->select(); + + if ($where instanceof \Closure) { + $where($select); + } elseif ($where !== null) { + $select->where($where); + } + + return $this->selectWith($select); + } + + /** + * @param Select $select + * @return null|ResultSetInterface + * @throws \RuntimeException + */ + public function selectWith(Select $select) + { + if (!$this->isInitialized) { + $this->initialize(); + } + return $this->executeSelect($select); + } + + /** + * @param Select $select + * @return ResultSet + * @throws Exception\RuntimeException + */ + protected function executeSelect(Select $select) + { + $selectState = $select->getRawState(); + if ($selectState['table'] != $this->table && (is_array($selectState['table']) && end($selectState['table']) != $this->table)) { + throw new Exception\RuntimeException('The table name of the provided select object must match that of the table'); + } + + if ($selectState['columns'] == array(Select::SQL_STAR) + && $this->columns !== array()) { + $select->columns($this->columns); + } + + // apply preSelect features + $this->featureSet->apply('preSelect', array($select)); + + // prepare and execute + $statement = $this->sql->prepareStatementForSqlObject($select); + $result = $statement->execute(); + + // build result set + $resultSet = clone $this->resultSetPrototype; + $resultSet->initialize($result); + + // apply postSelect features + $this->featureSet->apply('postSelect', array($statement, $result, $resultSet)); + + return $resultSet; + } + + /** + * Insert + * + * @param array $set + * @return int + */ + public function insert($set) + { + if (!$this->isInitialized) { + $this->initialize(); + } + $insert = $this->sql->insert(); + $insert->values($set); + return $this->executeInsert($insert); + } + + /** + * @param Insert $insert + * @return mixed + */ + public function insertWith(Insert $insert) + { + if (!$this->isInitialized) { + $this->initialize(); + } + return $this->executeInsert($insert); + } + + /** + * @todo add $columns support + * + * @param Insert $insert + * @return mixed + * @throws Exception\RuntimeException + */ + protected function executeInsert(Insert $insert) + { + $insertState = $insert->getRawState(); + if ($insertState['table'] != $this->table) { + throw new Exception\RuntimeException('The table name of the provided Insert object must match that of the table'); + } + + // apply preInsert features + $this->featureSet->apply('preInsert', array($insert)); + + $statement = $this->sql->prepareStatementForSqlObject($insert); + $result = $statement->execute(); + $this->lastInsertValue = $this->adapter->getDriver()->getConnection()->getLastGeneratedValue(); + + // apply postInsert features + $this->featureSet->apply('postInsert', array($statement, $result)); + + return $result->getAffectedRows(); + } + + /** + * Update + * + * @param array $set + * @param string|array|\Closure $where + * @return int + */ + public function update($set, $where = null) + { + if (!$this->isInitialized) { + $this->initialize(); + } + $sql = $this->sql; + $update = $sql->update(); + $update->set($set); + if ($where !== null) { + $update->where($where); + } + return $this->executeUpdate($update); + } + + /** + * @param \Zend\Db\Sql\Update $update + * @return mixed + */ + public function updateWith(Update $update) + { + if (!$this->isInitialized) { + $this->initialize(); + } + return $this->executeUpdate($update); + } + + /** + * @todo add $columns support + * + * @param Update $update + * @return mixed + * @throws Exception\RuntimeException + */ + protected function executeUpdate(Update $update) + { + $updateState = $update->getRawState(); + if ($updateState['table'] != $this->table) { + throw new Exception\RuntimeException('The table name of the provided Update object must match that of the table'); + } + + // apply preUpdate features + $this->featureSet->apply('preUpdate', array($update)); + + $statement = $this->sql->prepareStatementForSqlObject($update); + $result = $statement->execute(); + + // apply postUpdate features + $this->featureSet->apply('postUpdate', array($statement, $result)); + + return $result->getAffectedRows(); + } + + /** + * Delete + * + * @param Where|\Closure|string|array $where + * @return int + */ + public function delete($where) + { + if (!$this->isInitialized) { + $this->initialize(); + } + $delete = $this->sql->delete(); + if ($where instanceof \Closure) { + $where($delete); + } else { + $delete->where($where); + } + return $this->executeDelete($delete); + } + + /** + * @param Delete $delete + * @return mixed + */ + public function deleteWith(Delete $delete) + { + $this->initialize(); + return $this->executeDelete($delete); + } + + /** + * @todo add $columns support + * + * @param Delete $delete + * @return mixed + * @throws Exception\RuntimeException + */ + protected function executeDelete(Delete $delete) + { + $deleteState = $delete->getRawState(); + if ($deleteState['table'] != $this->table) { + throw new Exception\RuntimeException('The table name of the provided Update object must match that of the table'); + } + + // pre delete update + $this->featureSet->apply('preDelete', array($delete)); + + $statement = $this->sql->prepareStatementForSqlObject($delete); + $result = $statement->execute(); + + // apply postDelete features + $this->featureSet->apply('postDelete', array($statement, $result)); + + return $result->getAffectedRows(); + } + + /** + * Get last insert value + * + * @return int + */ + public function getLastInsertValue() + { + return $this->lastInsertValue; + } + + /** + * __get + * + * @param string $property + * @throws Exception\InvalidArgumentException + * @return mixed + */ + public function __get($property) + { + switch (strtolower($property)) { + case 'lastinsertvalue': + return $this->lastInsertValue; + case 'adapter': + return $this->adapter; + case 'table': + return $this->table; + } + if ($this->featureSet->canCallMagicGet($property)) { + return $this->featureSet->callMagicGet($property); + } + throw new Exception\InvalidArgumentException('Invalid magic property access in ' . __CLASS__ . '::__get()'); + } + + /** + * @param string $property + * @param mixed $value + * @return mixed + * @throws Exception\InvalidArgumentException + */ + public function __set($property, $value) + { + if ($this->featureSet->canCallMagicSet($property)) { + return $this->featureSet->callMagicSet($property, $value); + } + throw new Exception\InvalidArgumentException('Invalid magic property access in ' . __CLASS__ . '::__set()'); + } + + /** + * @param $method + * @param $arguments + * @return mixed + * @throws Exception\InvalidArgumentException + */ + public function __call($method, $arguments) + { + if ($this->featureSet->canCallMagicCall($method)) { + return $this->featureSet->callMagicCall($method, $arguments); + } + throw new Exception\InvalidArgumentException('Invalid method (' . $method . ') called, caught by ' . __CLASS__ . '::__call()'); + } + + /** + * __clone + */ + public function __clone() + { + $this->resultSetPrototype = (isset($this->resultSetPrototype)) ? clone $this->resultSetPrototype : null; + $this->sql = clone $this->sql; + if (is_object($this->table)) { + $this->table = clone $this->table; + } + } +} diff --git a/library/Zend/Db/TableGateway/Exception/ExceptionInterface.php b/library/Zend/Db/TableGateway/Exception/ExceptionInterface.php new file mode 100755 index 0000000000..ecd3085ac6 --- /dev/null +++ b/library/Zend/Db/TableGateway/Exception/ExceptionInterface.php @@ -0,0 +1,16 @@ +tableGateway = $tableGateway; + } + + public function initialize() + { + throw new Exception\RuntimeException('This method is not intended to be called on this object.'); + } + + public function getMagicMethodSpecifications() + { + return array(); + } + + + /* + public function preInitialize(); + public function postInitialize(); + public function preSelect(Select $select); + public function postSelect(StatementInterface $statement, ResultInterface $result, ResultSetInterface $resultSet); + public function preInsert(Insert $insert); + public function postInsert(StatementInterface $statement, ResultInterface $result); + public function preUpdate(Update $update); + public function postUpdate(StatementInterface $statement, ResultInterface $result); + public function preDelete(Delete $delete); + public function postDelete(StatementInterface $statement, ResultInterface $result); + */ +} diff --git a/library/Zend/Db/TableGateway/Feature/EventFeature.php b/library/Zend/Db/TableGateway/Feature/EventFeature.php new file mode 100755 index 0000000000..08e3cffbdc --- /dev/null +++ b/library/Zend/Db/TableGateway/Feature/EventFeature.php @@ -0,0 +1,255 @@ +eventManager = ($eventManager instanceof EventManagerInterface) + ? $eventManager + : new EventManager; + + $this->eventManager->addIdentifiers(array( + 'Zend\Db\TableGateway\TableGateway', + )); + + $this->event = ($tableGatewayEvent) ?: new EventFeature\TableGatewayEvent(); + } + + /** + * Retrieve composed event manager instance + * + * @return EventManagerInterface + */ + public function getEventManager() + { + return $this->eventManager; + } + + /** + * Retrieve composed event instance + * + * @return EventFeature\TableGatewayEvent + */ + public function getEvent() + { + return $this->event; + } + + /** + * Initialize feature and trigger "preInitialize" event + * + * Ensures that the composed TableGateway has identifiers based on the + * class name, and that the event target is set to the TableGateway + * instance. It then triggers the "preInitialize" event. + * + * @return void + */ + public function preInitialize() + { + if (get_class($this->tableGateway) != 'Zend\Db\TableGateway\TableGateway') { + $this->eventManager->addIdentifiers(get_class($this->tableGateway)); + } + + $this->event->setTarget($this->tableGateway); + $this->event->setName(__FUNCTION__); + $this->eventManager->trigger($this->event); + } + + /** + * Trigger the "postInitialize" event + * + * @return void + */ + public function postInitialize() + { + $this->event->setName(__FUNCTION__); + $this->eventManager->trigger($this->event); + } + + /** + * Trigger the "preSelect" event + * + * Triggers the "preSelect" event mapping the following parameters: + * - $select as "select" + * + * @param Select $select + * @return void + */ + public function preSelect(Select $select) + { + $this->event->setName(__FUNCTION__); + $this->event->setParams(array('select' => $select)); + $this->eventManager->trigger($this->event); + } + + /** + * Trigger the "postSelect" event + * + * Triggers the "postSelect" event mapping the following parameters: + * - $statement as "statement" + * - $result as "result" + * - $resultSet as "result_set" + * + * @param StatementInterface $statement + * @param ResultInterface $result + * @param ResultSetInterface $resultSet + * @return void + */ + public function postSelect(StatementInterface $statement, ResultInterface $result, ResultSetInterface $resultSet) + { + $this->event->setName(__FUNCTION__); + $this->event->setParams(array( + 'statement' => $statement, + 'result' => $result, + 'result_set' => $resultSet + )); + $this->eventManager->trigger($this->event); + } + + /** + * Trigger the "preInsert" event + * + * Triggers the "preInsert" event mapping the following parameters: + * - $insert as "insert" + * + * @param Insert $insert + * @return void + */ + public function preInsert(Insert $insert) + { + $this->event->setName(__FUNCTION__); + $this->event->setParams(array('insert' => $insert)); + $this->eventManager->trigger($this->event); + } + + /** + * Trigger the "postInsert" event + * + * Triggers the "postInsert" event mapping the following parameters: + * - $statement as "statement" + * - $result as "result" + * + * @param StatementInterface $statement + * @param ResultInterface $result + * @return void + */ + public function postInsert(StatementInterface $statement, ResultInterface $result) + { + $this->event->setName(__FUNCTION__); + $this->event->setParams(array( + 'statement' => $statement, + 'result' => $result, + )); + $this->eventManager->trigger($this->event); + } + + /** + * Trigger the "preUpdate" event + * + * Triggers the "preUpdate" event mapping the following parameters: + * - $update as "update" + * + * @param Update $update + * @return void + */ + public function preUpdate(Update $update) + { + $this->event->setName(__FUNCTION__); + $this->event->setParams(array('update' => $update)); + $this->eventManager->trigger($this->event); + } + + /** + * Trigger the "postUpdate" event + * + * Triggers the "postUpdate" event mapping the following parameters: + * - $statement as "statement" + * - $result as "result" + * + * @param StatementInterface $statement + * @param ResultInterface $result + * @return void + */ + public function postUpdate(StatementInterface $statement, ResultInterface $result) + { + $this->event->setName(__FUNCTION__); + $this->event->setParams(array( + 'statement' => $statement, + 'result' => $result, + )); + $this->eventManager->trigger($this->event); + } + + /** + * Trigger the "preDelete" event + * + * Triggers the "preDelete" event mapping the following parameters: + * - $delete as "delete" + * + * @param Delete $delete + * @return void + */ + public function preDelete(Delete $delete) + { + $this->event->setName(__FUNCTION__); + $this->event->setParams(array('delete' => $delete)); + $this->eventManager->trigger($this->event); + } + + /** + * Trigger the "postDelete" event + * + * Triggers the "postDelete" event mapping the following parameters: + * - $statement as "statement" + * - $result as "result" + * + * @param StatementInterface $statement + * @param ResultInterface $result + * @return void + */ + public function postDelete(StatementInterface $statement, ResultInterface $result) + { + $this->event->setName(__FUNCTION__); + $this->event->setParams(array( + 'statement' => $statement, + 'result' => $result, + )); + $this->eventManager->trigger($this->event); + } +} diff --git a/library/Zend/Db/TableGateway/Feature/EventFeature/TableGatewayEvent.php b/library/Zend/Db/TableGateway/Feature/EventFeature/TableGatewayEvent.php new file mode 100755 index 0000000000..1097a86fe5 --- /dev/null +++ b/library/Zend/Db/TableGateway/Feature/EventFeature/TableGatewayEvent.php @@ -0,0 +1,139 @@ +name; + } + + /** + * Get target/context from which event was triggered + * + * @return null|string|object + */ + public function getTarget() + { + return $this->target; + } + + /** + * Get parameters passed to the event + * + * @return array|\ArrayAccess + */ + public function getParams() + { + return $this->params; + } + + /** + * Get a single parameter by name + * + * @param string $name + * @param mixed $default Default value to return if parameter does not exist + * @return mixed + */ + public function getParam($name, $default = null) + { + return (isset($this->params[$name]) ? $this->params[$name] : $default); + } + + /** + * Set the event name + * + * @param string $name + * @return void + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Set the event target/context + * + * @param null|string|object $target + * @return void + */ + public function setTarget($target) + { + $this->target = $target; + } + + /** + * Set event parameters + * + * @param string $params + * @return void + */ + public function setParams($params) + { + $this->params = $params; + } + + /** + * Set a single parameter by key + * + * @param string $name + * @param mixed $value + * @return void + */ + public function setParam($name, $value) + { + $this->params[$name] = $value; + } + + /** + * Indicate whether or not the parent EventManagerInterface should stop propagating events + * + * @param bool $flag + * @return void + */ + public function stopPropagation($flag = true) + { + return; + } + + /** + * Has this event indicated event propagation should stop? + * + * @return bool + */ + public function propagationIsStopped() + { + return false; + } +} diff --git a/library/Zend/Db/TableGateway/Feature/FeatureSet.php b/library/Zend/Db/TableGateway/Feature/FeatureSet.php new file mode 100755 index 0000000000..498db1ad7f --- /dev/null +++ b/library/Zend/Db/TableGateway/Feature/FeatureSet.php @@ -0,0 +1,146 @@ +addFeatures($features); + } + } + + public function setTableGateway(AbstractTableGateway $tableGateway) + { + $this->tableGateway = $tableGateway; + foreach ($this->features as $feature) { + $feature->setTableGateway($this->tableGateway); + } + return $this; + } + + public function getFeatureByClassName($featureClassName) + { + $feature = false; + foreach ($this->features as $potentialFeature) { + if ($potentialFeature instanceof $featureClassName) { + $feature = $potentialFeature; + break; + } + } + return $feature; + } + + public function addFeatures(array $features) + { + foreach ($features as $feature) { + $this->addFeature($feature); + } + return $this; + } + + public function addFeature(AbstractFeature $feature) + { + if ($this->tableGateway instanceof TableGatewayInterface) { + $feature->setTableGateway($this->tableGateway); + } + $this->features[] = $feature; + return $this; + } + + public function apply($method, $args) + { + foreach ($this->features as $feature) { + if (method_exists($feature, $method)) { + $return = call_user_func_array(array($feature, $method), $args); + if ($return === self::APPLY_HALT) { + break; + } + } + } + } + + /** + * @param string $property + * @return bool + */ + public function canCallMagicGet($property) + { + return false; + } + + /** + * @param string $property + * @return mixed + */ + public function callMagicGet($property) + { + $return = null; + return $return; + } + + /** + * @param string $property + * @return bool + */ + public function canCallMagicSet($property) + { + return false; + } + + /** + * @param $property + * @param $value + * @return mixed + */ + public function callMagicSet($property, $value) + { + $return = null; + return $return; + } + + /** + * @param string $method + * @return bool + */ + public function canCallMagicCall($method) + { + return false; + } + + /** + * @param string $method + * @param array $arguments + * @return mixed + */ + public function callMagicCall($method, $arguments) + { + $return = null; + return $return; + } +} diff --git a/library/Zend/Db/TableGateway/Feature/GlobalAdapterFeature.php b/library/Zend/Db/TableGateway/Feature/GlobalAdapterFeature.php new file mode 100755 index 0000000000..bb8c6d60bc --- /dev/null +++ b/library/Zend/Db/TableGateway/Feature/GlobalAdapterFeature.php @@ -0,0 +1,67 @@ +tableGateway->adapter = self::getStaticAdapter(); + } +} diff --git a/library/Zend/Db/TableGateway/Feature/MasterSlaveFeature.php b/library/Zend/Db/TableGateway/Feature/MasterSlaveFeature.php new file mode 100755 index 0000000000..255735de88 --- /dev/null +++ b/library/Zend/Db/TableGateway/Feature/MasterSlaveFeature.php @@ -0,0 +1,91 @@ +slaveAdapter = $slaveAdapter; + if ($slaveSql) { + $this->slaveSql = $slaveSql; + } + } + + public function getSlaveAdapter() + { + return $this->slaveAdapter; + } + + /** + * @return Sql + */ + public function getSlaveSql() + { + return $this->slaveSql; + } + + /** + * after initialization, retrieve the original adapter as "master" + */ + public function postInitialize() + { + $this->masterSql = $this->tableGateway->sql; + if ($this->slaveSql == null) { + $this->slaveSql = new Sql( + $this->slaveAdapter, + $this->tableGateway->sql->getTable(), + $this->tableGateway->sql->getSqlPlatform() + ); + } + } + + /** + * preSelect() + * Replace adapter with slave temporarily + */ + public function preSelect() + { + $this->tableGateway->sql = $this->slaveSql; + } + + /** + * postSelect() + * Ensure to return to the master adapter + */ + public function postSelect() + { + $this->tableGateway->sql = $this->masterSql; + } +} diff --git a/library/Zend/Db/TableGateway/Feature/MetadataFeature.php b/library/Zend/Db/TableGateway/Feature/MetadataFeature.php new file mode 100755 index 0000000000..59393bec2a --- /dev/null +++ b/library/Zend/Db/TableGateway/Feature/MetadataFeature.php @@ -0,0 +1,85 @@ +metadata = $metadata; + } + $this->sharedData['metadata'] = array( + 'primaryKey' => null, + 'columns' => array() + ); + } + + public function postInitialize() + { + if ($this->metadata == null) { + $this->metadata = new Metadata($this->tableGateway->adapter); + } + + // localize variable for brevity + $t = $this->tableGateway; + $m = $this->metadata; + + // get column named + $columns = $m->getColumnNames($t->table); + $t->columns = $columns; + + // set locally + $this->sharedData['metadata']['columns'] = $columns; + + // process primary key only if table is a table; there are no PK constraints on views + if (!($m->getTable($t->table) instanceof TableObject)) { + return; + } + + $pkc = null; + + foreach ($m->getConstraints($t->table) as $constraint) { + /** @var $constraint \Zend\Db\Metadata\Object\ConstraintObject */ + if ($constraint->getType() == 'PRIMARY KEY') { + $pkc = $constraint; + break; + } + } + + if ($pkc === null) { + throw new Exception\RuntimeException('A primary key for this column could not be found in the metadata.'); + } + + if (count($pkc->getColumns()) == 1) { + $pkck = $pkc->getColumns(); + $primaryKey = $pkck[0]; + } else { + $primaryKey = $pkc->getColumns(); + } + + $this->sharedData['metadata']['primaryKey'] = $primaryKey; + } +} diff --git a/library/Zend/Db/TableGateway/Feature/RowGatewayFeature.php b/library/Zend/Db/TableGateway/Feature/RowGatewayFeature.php new file mode 100755 index 0000000000..18c4fd9df4 --- /dev/null +++ b/library/Zend/Db/TableGateway/Feature/RowGatewayFeature.php @@ -0,0 +1,67 @@ +constructorArguments = func_get_args(); + } + + public function postInitialize() + { + $args = $this->constructorArguments; + + /** @var $resultSetPrototype ResultSet */ + $resultSetPrototype = $this->tableGateway->resultSetPrototype; + + if (!$this->tableGateway->resultSetPrototype instanceof ResultSet) { + throw new Exception\RuntimeException( + 'This feature ' . __CLASS__ . ' expects the ResultSet to be an instance of Zend\Db\ResultSet\ResultSet' + ); + } + + if (isset($args[0])) { + if (is_string($args[0])) { + $primaryKey = $args[0]; + $rowGatewayPrototype = new RowGateway($primaryKey, $this->tableGateway->table, $this->tableGateway->adapter, $this->tableGateway->sql); + $resultSetPrototype->setArrayObjectPrototype($rowGatewayPrototype); + } elseif ($args[0] instanceof RowGatewayInterface) { + $rowGatewayPrototype = $args[0]; + $resultSetPrototype->setArrayObjectPrototype($rowGatewayPrototype); + } + } else { + // get from metadata feature + $metadata = $this->tableGateway->featureSet->getFeatureByClassName('Zend\Db\TableGateway\Feature\MetadataFeature'); + if ($metadata === false || !isset($metadata->sharedData['metadata'])) { + throw new Exception\RuntimeException( + 'No information was provided to the RowGatewayFeature and/or no MetadataFeature could be consulted to find the primary key necessary for RowGateway object creation.' + ); + } + $primaryKey = $metadata->sharedData['metadata']['primaryKey']; + $rowGatewayPrototype = new RowGateway($primaryKey, $this->tableGateway->table, $this->tableGateway->adapter, $this->tableGateway->sql); + $resultSetPrototype->setArrayObjectPrototype($rowGatewayPrototype); + } + } +} diff --git a/library/Zend/Db/TableGateway/Feature/SequenceFeature.php b/library/Zend/Db/TableGateway/Feature/SequenceFeature.php new file mode 100755 index 0000000000..9f58d1a569 --- /dev/null +++ b/library/Zend/Db/TableGateway/Feature/SequenceFeature.php @@ -0,0 +1,133 @@ +primaryKeyField = $primaryKeyField; + $this->sequenceName = $sequenceName; + } + + /** + * @param Insert $insert + * @return Insert + */ + public function preInsert(Insert $insert) + { + $columns = $insert->getRawState('columns'); + $values = $insert->getRawState('values'); + $key = array_search($this->primaryKeyField, $columns); + if ($key !== false) { + $this->sequenceValue = $values[$key]; + return $insert; + } + + $this->sequenceValue = $this->nextSequenceId(); + if ($this->sequenceValue === null) { + return $insert; + } + + $insert->values(array($this->primaryKeyField => $this->sequenceValue), Insert::VALUES_MERGE); + return $insert; + } + + /** + * @param StatementInterface $statement + * @param ResultInterface $result + */ + public function postInsert(StatementInterface $statement, ResultInterface $result) + { + if ($this->sequenceValue !== null) { + $this->tableGateway->lastInsertValue = $this->sequenceValue; + } + } + + /** + * Generate a new value from the specified sequence in the database, and return it. + * @return int + */ + public function nextSequenceId() + { + $platform = $this->tableGateway->adapter->getPlatform(); + $platformName = $platform->getName(); + + switch ($platformName) { + case 'Oracle': + $sql = 'SELECT ' . $platform->quoteIdentifier($this->sequenceName) . '.NEXTVAL as "nextval" FROM dual'; + break; + case 'PostgreSQL': + $sql = 'SELECT NEXTVAL(\'"' . $this->sequenceName . '"\')'; + break; + default : + return null; + } + + $statement = $this->tableGateway->adapter->createStatement(); + $statement->prepare($sql); + $result = $statement->execute(); + $sequence = $result->current(); + unset($statement, $result); + return $sequence['nextval']; + } + + /** + * Return the most recent value from the specified sequence in the database. + * @return int + */ + public function lastSequenceId() + { + $platform = $this->tableGateway->adapter->getPlatform(); + $platformName = $platform->getName(); + + switch ($platformName) { + case 'Oracle': + $sql = 'SELECT ' . $platform->quoteIdentifier($this->sequenceName) . '.CURRVAL as "currval" FROM dual'; + break; + case 'PostgreSQL': + $sql = 'SELECT CURRVAL(\'' . $this->sequenceName . '\')'; + break; + default : + return null; + } + + $statement = $this->tableGateway->adapter->createStatement(); + $statement->prepare($sql); + $result = $statement->execute(); + $sequence = $result->current(); + unset($statement, $result); + return $sequence['currval']; + } +} diff --git a/library/Zend/Db/TableGateway/TableGateway.php b/library/Zend/Db/TableGateway/TableGateway.php new file mode 100755 index 0000000000..0defd8a4d7 --- /dev/null +++ b/library/Zend/Db/TableGateway/TableGateway.php @@ -0,0 +1,72 @@ +table = $table; + + // adapter + $this->adapter = $adapter; + + // process features + if ($features !== null) { + if ($features instanceof Feature\AbstractFeature) { + $features = array($features); + } + if (is_array($features)) { + $this->featureSet = new Feature\FeatureSet($features); + } elseif ($features instanceof Feature\FeatureSet) { + $this->featureSet = $features; + } else { + throw new Exception\InvalidArgumentException( + 'TableGateway expects $feature to be an instance of an AbstractFeature or a FeatureSet, or an array of AbstractFeatures' + ); + } + } else { + $this->featureSet = new Feature\FeatureSet(); + } + + // result prototype + $this->resultSetPrototype = ($resultSetPrototype) ?: new ResultSet; + + // Sql object (factory for select, insert, update, delete) + $this->sql = ($sql) ?: new Sql($this->adapter, $this->table); + + // check sql object bound to same table + if ($this->sql->getTable() != $this->table) { + throw new Exception\InvalidArgumentException('The table inside the provided Sql object must match the table of this TableGateway'); + } + + $this->initialize(); + } +} diff --git a/library/Zend/Db/TableGateway/TableGatewayInterface.php b/library/Zend/Db/TableGateway/TableGatewayInterface.php new file mode 100755 index 0000000000..0a77e0f1d7 --- /dev/null +++ b/library/Zend/Db/TableGateway/TableGatewayInterface.php @@ -0,0 +1,19 @@ +=5.3.23" + }, + "require-dev": { + "zendframework/zend-eventmanager": "self.version", + "zendframework/zend-servicemanager": "self.version", + "zendframework/zend-stdlib": "self.version" + }, + "suggest": { + "zendframework/zend-eventmanager": "Zend\\EventManager component", + "zendframework/zend-servicemanager": "Zend\\ServiceManager component", + "zendframework/zend-stdlib": "Zend\\Stdlib component" + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev", + "dev-develop": "2.4-dev" + } + } +} diff --git a/library/Zend/Debug/CONTRIBUTING.md b/library/Zend/Debug/CONTRIBUTING.md new file mode 100755 index 0000000000..e77f5d2d5b --- /dev/null +++ b/library/Zend/Debug/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# CONTRIBUTING + +Please don't open pull requests against this repository, please use https://github.com/zendframework/zf2. \ No newline at end of file diff --git a/library/Zend/Debug/Debug.php b/library/Zend/Debug/Debug.php new file mode 100755 index 0000000000..f240935c85 --- /dev/null +++ b/library/Zend/Debug/Debug.php @@ -0,0 +1,124 @@ + tags, cleans up newlines and indents, and runs + * htmlentities() before output. + * + * @param mixed $var The variable to dump. + * @param string $label OPTIONAL Label to prepend to output. + * @param bool $echo OPTIONAL Echo output if true. + * @return string + */ + public static function dump($var, $label = null, $echo = true) + { + // format the label + $label = ($label===null) ? '' : rtrim($label) . ' '; + + // var_dump the variable into a buffer and keep the output + ob_start(); + var_dump($var); + $output = ob_get_clean(); + + // neaten the newlines and indents + $output = preg_replace("/\]\=\>\n(\s+)/m", "] => ", $output); + if (static::getSapi() == 'cli') { + $output = PHP_EOL . $label + . PHP_EOL . $output + . PHP_EOL; + } else { + if (null !== static::$escaper) { + $output = static::$escaper->escapeHtml($output); + } elseif (!extension_loaded('xdebug')) { + $output = static::getEscaper()->escapeHtml($output); + } + + $output = '

'
+                    . $label
+                    . $output
+                    . '
'; + } + + if ($echo) { + echo $output; + } + return $output; + } +} diff --git a/library/Zend/Debug/README.md b/library/Zend/Debug/README.md new file mode 100755 index 0000000000..8797b8e216 --- /dev/null +++ b/library/Zend/Debug/README.md @@ -0,0 +1,15 @@ +Debug Component from ZF2 +======================== + +This is the Debug component for ZF2. + +- File issues at https://github.com/zendframework/zf2/issues +- Create pull requests against https://github.com/zendframework/zf2 +- Documentation is at http://framework.zend.com/docs + +LICENSE +------- + +The files in this archive are released under the [Zend Framework +license](http://framework.zend.com/license), which is a 3-clause BSD license. + diff --git a/library/Zend/Debug/composer.json b/library/Zend/Debug/composer.json new file mode 100755 index 0000000000..b6c18c7bf6 --- /dev/null +++ b/library/Zend/Debug/composer.json @@ -0,0 +1,32 @@ +{ + "name": "zendframework/zend-debug", + "description": " ", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "debug" + ], + "homepage": "https://github.com/zendframework/zf2", + "autoload": { + "psr-0": { + "Zend\\Debug\\": "" + } + }, + "target-dir": "Zend/Debug", + "require": { + "php": ">=5.3.23" + }, + "require-dev": { + "zendframework/zend-escaper": "*" + }, + "suggest": { + "ext/xdebug": "XDebug, for better backtrace output", + "zendframework/zend-escaper": "To support escaped output" + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev", + "dev-develop": "2.4-dev" + } + } +} diff --git a/library/Zend/Di/CONTRIBUTING.md b/library/Zend/Di/CONTRIBUTING.md new file mode 100755 index 0000000000..e77f5d2d5b --- /dev/null +++ b/library/Zend/Di/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# CONTRIBUTING + +Please don't open pull requests against this repository, please use https://github.com/zendframework/zf2. \ No newline at end of file diff --git a/library/Zend/Di/Config.php b/library/Zend/Di/Config.php new file mode 100755 index 0000000000..b177305045 --- /dev/null +++ b/library/Zend/Di/Config.php @@ -0,0 +1,196 @@ +data = $options; + } + + /** + * Configure + * + * @param Di $di + * @return void + */ + public function configure(Di $di) + { + if (isset($this->data['definition'])) { + $this->configureDefinition($di, $this->data['definition']); + } + if (isset($this->data['instance'])) { + $this->configureInstance($di, $this->data['instance']); + } + } + + /** + * @param Di $di + * @param array $definition + */ + public function configureDefinition(Di $di, $definition) + { + foreach ($definition as $definitionType => $definitionData) { + switch ($definitionType) { + case 'compiler': + foreach ($definitionData as $filename) { + if (is_readable($filename)) { + $di->definitions()->addDefinition(new ArrayDefinition(include $filename), false); + } + } + break; + case 'runtime': + if (isset($definitionData['enabled']) && !$definitionData['enabled']) { + // Remove runtime from definition list if not enabled + $definitions = array(); + foreach ($di->definitions() as $definition) { + if (!$definition instanceof RuntimeDefinition) { + $definitions[] = $definition; + } + } + $definitionList = new DefinitionList($definitions); + $di->setDefinitionList($definitionList); + } elseif (isset($definitionData['use_annotations']) && $definitionData['use_annotations']) { + /* @var $runtimeDefinition Definition\RuntimeDefinition */ + $runtimeDefinition = $di + ->definitions() + ->getDefinitionByType('\Zend\Di\Definition\RuntimeDefinition'); + $runtimeDefinition->getIntrospectionStrategy()->setUseAnnotations(true); + } + break; + case 'class': + foreach ($definitionData as $className => $classData) { + $classDefinitions = $di->definitions()->getDefinitionsByType('Zend\Di\Definition\ClassDefinition'); + foreach ($classDefinitions as $classDefinition) { + if (!$classDefinition->hasClass($className)) { + unset($classDefinition); + } + } + if (!isset($classDefinition)) { + $classDefinition = new Definition\ClassDefinition($className); + $di->definitions()->addDefinition($classDefinition, false); + } + foreach ($classData as $classDefKey => $classDefData) { + switch ($classDefKey) { + case 'instantiator': + $classDefinition->setInstantiator($classDefData); + break; + case 'supertypes': + $classDefinition->setSupertypes($classDefData); + break; + case 'methods': + case 'method': + foreach ($classDefData as $methodName => $methodInfo) { + if (isset($methodInfo['required'])) { + $classDefinition->addMethod($methodName, $methodInfo['required']); + unset($methodInfo['required']); + } + foreach ($methodInfo as $paramName => $paramInfo) { + $classDefinition->addMethodParameter($methodName, $paramName, $paramInfo); + } + } + break; + default: + $methodName = $classDefKey; + $methodInfo = $classDefData; + if (isset($classDefData['required'])) { + $classDefinition->addMethod($methodName, $methodInfo['required']); + unset($methodInfo['required']); + } + foreach ($methodInfo as $paramName => $paramInfo) { + $classDefinition->addMethodParameter($methodName, $paramName, $paramInfo); + } + } + } + } + } + } + } + + /** + * Configures a given Di instance + * + * @param Di $di + * @param $instanceData + */ + public function configureInstance(Di $di, $instanceData) + { + $im = $di->instanceManager(); + + foreach ($instanceData as $target => $data) { + switch (strtolower($target)) { + case 'aliases': + case 'alias': + foreach ($data as $n => $v) { + $im->addAlias($n, $v); + } + break; + case 'preferences': + case 'preference': + foreach ($data as $n => $v) { + if (is_array($v)) { + foreach ($v as $v2) { + $im->addTypePreference($n, $v2); + } + } else { + $im->addTypePreference($n, $v); + } + } + break; + default: + foreach ($data as $n => $v) { + switch ($n) { + case 'parameters': + case 'parameter': + $im->setParameters($target, $v); + break; + case 'injections': + case 'injection': + $im->setInjections($target, $v); + break; + case 'shared': + case 'share': + $im->setShared($target, $v); + break; + } + } + } + } + } +} diff --git a/library/Zend/Di/Definition/Annotation/Inject.php b/library/Zend/Di/Definition/Annotation/Inject.php new file mode 100755 index 0000000000..8534c02151 --- /dev/null +++ b/library/Zend/Di/Definition/Annotation/Inject.php @@ -0,0 +1,31 @@ +content = $content; + } +} diff --git a/library/Zend/Di/Definition/Annotation/Instantiator.php b/library/Zend/Di/Definition/Annotation/Instantiator.php new file mode 100755 index 0000000000..d0aed5310d --- /dev/null +++ b/library/Zend/Di/Definition/Annotation/Instantiator.php @@ -0,0 +1,31 @@ +content = $content; + } +} diff --git a/library/Zend/Di/Definition/ArrayDefinition.php b/library/Zend/Di/Definition/ArrayDefinition.php new file mode 100755 index 0000000000..5e64f5b38f --- /dev/null +++ b/library/Zend/Di/Definition/ArrayDefinition.php @@ -0,0 +1,176 @@ + $value) { + // force lower names + $dataArray[$class] = array_change_key_case($dataArray[$class], CASE_LOWER); + } + foreach ($dataArray as $class => $definition) { + if (isset($definition['methods']) && is_array($definition['methods'])) { + foreach ($definition['methods'] as $type => $requirement) { + if (!is_int($requirement)) { + $dataArray[$class]['methods'][$type] = InjectionMethod::detectMethodRequirement($requirement); + } + } + } + } + $this->dataArray = $dataArray; + } + + /** + * {@inheritDoc} + */ + public function getClasses() + { + return array_keys($this->dataArray); + } + + /** + * {@inheritDoc} + */ + public function hasClass($class) + { + return array_key_exists($class, $this->dataArray); + } + + /** + * {@inheritDoc} + */ + public function getClassSupertypes($class) + { + if (!isset($this->dataArray[$class])) { + return array(); + } + + if (!isset($this->dataArray[$class]['supertypes'])) { + return array(); + } + + return $this->dataArray[$class]['supertypes']; + } + + /** + * {@inheritDoc} + */ + public function getInstantiator($class) + { + if (!isset($this->dataArray[$class])) { + return null; + } + + if (!isset($this->dataArray[$class]['instantiator'])) { + return '__construct'; + } + + return $this->dataArray[$class]['instantiator']; + } + + /** + * {@inheritDoc} + */ + public function hasMethods($class) + { + if (!isset($this->dataArray[$class])) { + return false; + } + + if (!isset($this->dataArray[$class]['methods'])) { + return false; + } + + return (count($this->dataArray[$class]['methods']) > 0); + } + + /** + * {@inheritDoc} + */ + public function hasMethod($class, $method) + { + if (!isset($this->dataArray[$class])) { + return false; + } + + if (!isset($this->dataArray[$class]['methods'])) { + return false; + } + + return array_key_exists($method, $this->dataArray[$class]['methods']); + } + + /** + * {@inheritDoc} + */ + public function getMethods($class) + { + if (!isset($this->dataArray[$class])) { + return array(); + } + + if (!isset($this->dataArray[$class]['methods'])) { + return array(); + } + + return $this->dataArray[$class]['methods']; + } + + /** + * {@inheritDoc} + */ + public function hasMethodParameters($class, $method) + { + return isset($this->dataArray[$class]['parameters'][$method]); + } + + /** + * {@inheritDoc} + */ + public function getMethodParameters($class, $method) + { + if (!isset($this->dataArray[$class])) { + return array(); + } + + if (!isset($this->dataArray[$class]['parameters'])) { + return array(); + } + + if (!isset($this->dataArray[$class]['parameters'][$method])) { + return array(); + } + + return $this->dataArray[$class]['parameters'][$method]; + } + + /** + * @return array + */ + public function toArray() + { + return $this->dataArray; + } +} diff --git a/library/Zend/Di/Definition/Builder/InjectionMethod.php b/library/Zend/Di/Definition/Builder/InjectionMethod.php new file mode 100755 index 0000000000..a27f7b1922 --- /dev/null +++ b/library/Zend/Di/Definition/Builder/InjectionMethod.php @@ -0,0 +1,121 @@ +name = $name; + + return $this; + } + + /** + * @return null|string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + * @param string|null $class + * @param mixed|null $isRequired + * @param mixed|null $default + * @return InjectionMethod + */ + public function addParameter($name, $class = null, $isRequired = null, $default = null) + { + $this->parameters[] = array( + $name, + $class, + self::detectMethodRequirement($isRequired), + $default, + ); + + return $this; + } + + /** + * @return array + */ + public function getParameters() + { + return $this->parameters; + } + + /** + * + * @param mixed $requirement + * @return int + */ + public static function detectMethodRequirement($requirement) + { + if (is_bool($requirement)) { + return $requirement ? Di::METHOD_IS_REQUIRED : Di::METHOD_IS_OPTIONAL; + } + + if (null === $requirement) { + //This is mismatch to ClassDefinition::addMethod is it ok ? is optional? + return Di::METHOD_IS_REQUIRED; + } + + if (is_int($requirement)) { + return $requirement; + } + + if (is_string($requirement)) { + switch (strtolower($requirement)) { + default: + case "require": + case "required": + return Di::METHOD_IS_REQUIRED; + break; + case "aware": + return Di::METHOD_IS_AWARE; + break; + case "optional": + return Di::METHOD_IS_OPTIONAL; + break; + case "constructor": + return Di::METHOD_IS_CONSTRUCTOR; + break; + case "instantiator": + return Di::METHOD_IS_INSTANTIATOR; + break; + case "eager": + return Di::METHOD_IS_EAGER; + break; + } + } + return 0; + } +} diff --git a/library/Zend/Di/Definition/Builder/PhpClass.php b/library/Zend/Di/Definition/Builder/PhpClass.php new file mode 100755 index 0000000000..80d4197a28 --- /dev/null +++ b/library/Zend/Di/Definition/Builder/PhpClass.php @@ -0,0 +1,175 @@ +name = $name; + + return $this; + } + + /** + * Get name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string|\Callable|array $instantiator + * @return PhpClass + */ + public function setInstantiator($instantiator) + { + $this->instantiator = $instantiator; + + return $this; + } + + /** + * @return array|\Callable|string + */ + public function getInstantiator() + { + return $this->instantiator; + } + + /** + * @param string $superType + * @return PhpClass + */ + public function addSuperType($superType) + { + $this->superTypes[] = $superType; + + return $this; + } + + /** + * Get super types + * + * @return array + */ + public function getSuperTypes() + { + return $this->superTypes; + } + + /** + * Add injection method + * + * @param InjectionMethod $injectionMethod + * @return PhpClass + */ + public function addInjectionMethod(InjectionMethod $injectionMethod) + { + $this->injectionMethods[] = $injectionMethod; + + return $this; + } + + /** + * Create and register an injection method + * + * Optionally takes the method name. + * + * This method may be used in lieu of addInjectionMethod() in + * order to provide a more fluent interface for building classes with + * injection methods. + * + * @param null|string $name + * @return InjectionMethod + */ + public function createInjectionMethod($name = null) + { + $builder = $this->defaultMethodBuilder; + /* @var $method InjectionMethod */ + $method = new $builder(); + if (null !== $name) { + $method->setName($name); + } + $this->addInjectionMethod($method); + + return $method; + } + + /** + * Override which class will be used by {@link createInjectionMethod()} + * + * @param string $class + * @return PhpClass + */ + public function setMethodBuilder($class) + { + $this->defaultMethodBuilder = $class; + + return $this; + } + + /** + * Determine what class will be used by {@link createInjectionMethod()} + * + * Mainly to provide the ability to temporarily override the class used. + * + * @return string + */ + public function getMethodBuilder() + { + return $this->defaultMethodBuilder; + } + + /** + * @return InjectionMethod[] + */ + public function getInjectionMethods() + { + return $this->injectionMethods; + } +} diff --git a/library/Zend/Di/Definition/BuilderDefinition.php b/library/Zend/Di/Definition/BuilderDefinition.php new file mode 100755 index 0000000000..6ad935a074 --- /dev/null +++ b/library/Zend/Di/Definition/BuilderDefinition.php @@ -0,0 +1,321 @@ + $classInfo) { + $class = new Builder\PhpClass(); + $class->setName($className); + foreach ($classInfo as $type => $typeData) { + switch (strtolower($type)) { + case 'supertypes': + foreach ($typeData as $superType) { + $class->addSuperType($superType); + } + break; + case 'instantiator': + $class->setInstantiator($typeData); + break; + case 'methods': + case 'method': + foreach ($typeData as $injectionMethodName => $injectionMethodData) { + $injectionMethod = new Builder\InjectionMethod(); + $injectionMethod->setName($injectionMethodName); + foreach ($injectionMethodData as $parameterName => $parameterType) { + $parameterType = ($parameterType) ?: null; // force empty string to null + $injectionMethod->addParameter($parameterName, $parameterType); + } + $class->addInjectionMethod($injectionMethod); + } + break; + + } + } + $this->addClass($class); + } + } + + /** + * Add class + * + * @param Builder\PhpClass $phpClass + * @return BuilderDefinition + */ + public function addClass(Builder\PhpClass $phpClass) + { + $this->classes[] = $phpClass; + + return $this; + } + + /** + * Create a class builder object using default class builder class + * + * This method is a factory that can be used in place of addClass(). + * + * @param null|string $name Optional name of class to assign + * @return Builder\PhpClass + */ + public function createClass($name = null) + { + $builderClass = $this->defaultClassBuilder; + /* @var $class Builder\PhpClass */ + $class = new $builderClass(); + + if (null !== $name) { + $class->setName($name); + } + + $this->addClass($class); + + return $class; + } + + /** + * Set the class to use with {@link createClass()} + * + * @param string $class + * @return BuilderDefinition + */ + public function setClassBuilder($class) + { + $this->defaultClassBuilder = $class; + + return $this; + } + + /** + * Get the class used for {@link createClass()} + * + * This is primarily to allow developers to temporarily override + * the builder strategy. + * + * @return string + */ + public function getClassBuilder() + { + return $this->defaultClassBuilder; + } + + /** + * {@inheritDoc} + */ + public function getClasses() + { + $classNames = array(); + + /* @var $class Builder\PhpClass */ + foreach ($this->classes as $class) { + $classNames[] = $class->getName(); + } + + return $classNames; + } + + /** + * {@inheritDoc} + */ + public function hasClass($class) + { + foreach ($this->classes as $classObj) { + if ($classObj->getName() === $class) { + return true; + } + } + + return false; + } + + /** + * @param string $name + * @return bool|Builder\PhpClass + */ + protected function getClass($name) + { + foreach ($this->classes as $classObj) { + if ($classObj->getName() === $name) { + return $classObj; + } + } + + return false; + } + + /** + * {@inheritDoc} + * @throws \Zend\Di\Exception\RuntimeException + */ + public function getClassSupertypes($class) + { + $class = $this->getClass($class); + + if ($class === false) { + throw new Exception\RuntimeException('Cannot find class object in this builder definition.'); + } + + return $class->getSuperTypes(); + } + + /** + * {@inheritDoc} + * @throws \Zend\Di\Exception\RuntimeException + */ + public function getInstantiator($class) + { + $class = $this->getClass($class); + if ($class === false) { + throw new Exception\RuntimeException('Cannot find class object in this builder definition.'); + } + + return $class->getInstantiator(); + } + + /** + * {@inheritDoc} + * @throws \Zend\Di\Exception\RuntimeException + */ + public function hasMethods($class) + { + /* @var $class \Zend\Di\Definition\Builder\PhpClass */ + $class = $this->getClass($class); + if ($class === false) { + throw new Exception\RuntimeException('Cannot find class object in this builder definition.'); + } + + return (count($class->getInjectionMethods()) > 0); + } + + /** + * {@inheritDoc} + * @throws \Zend\Di\Exception\RuntimeException + */ + public function getMethods($class) + { + $class = $this->getClass($class); + if ($class === false) { + throw new Exception\RuntimeException('Cannot find class object in this builder definition.'); + } + $methods = $class->getInjectionMethods(); + $methodNames = array(); + + /* @var $methodObj Builder\InjectionMethod */ + foreach ($methods as $methodObj) { + $methodNames[] = $methodObj->getName(); + } + + return $methodNames; + } + + /** + * {@inheritDoc} + * @throws \Zend\Di\Exception\RuntimeException + */ + public function hasMethod($class, $method) + { + $class = $this->getClass($class); + if ($class === false) { + throw new Exception\RuntimeException('Cannot find class object in this builder definition.'); + } + $methods = $class->getInjectionMethods(); + + /* @var $methodObj Builder\InjectionMethod */ + foreach ($methods as $methodObj) { + if ($methodObj->getName() === $method) { + return true; + } + } + + return false; + } + + /** + * {@inheritDoc} + */ + public function hasMethodParameters($class, $method) + { + $class = $this->getClass($class); + if ($class === false) { + return false; + } + $methods = $class->getInjectionMethods(); + /* @var $methodObj Builder\InjectionMethod */ + foreach ($methods as $methodObj) { + if ($methodObj->getName() === $method) { + $method = $methodObj; + } + } + if (!$method instanceof Builder\InjectionMethod) { + return false; + } + + /* @var $method Builder\InjectionMethod */ + + return (count($method->getParameters()) > 0); + } + + /** + * {@inheritDoc} + * @throws \Zend\Di\Exception\RuntimeException + */ + public function getMethodParameters($class, $method) + { + $class = $this->getClass($class); + + if ($class === false) { + throw new Exception\RuntimeException('Cannot find class object in this builder definition.'); + } + + $methods = $class->getInjectionMethods(); + + /* @var $methodObj Builder\InjectionMethod */ + foreach ($methods as $methodObj) { + if ($methodObj->getName() === $method) { + $method = $methodObj; + } + } + + if (!$method instanceof Builder\InjectionMethod) { + throw new Exception\RuntimeException('Cannot find method object for method ' . $method . ' in this builder definition.'); + } + + $methodParameters = array(); + + /* @var $method Builder\InjectionMethod */ + foreach ($method->getParameters() as $name => $info) { + $methodParameters[$class->getName() . '::' . $method->getName() . ':' . $name] = $info; + } + + return $methodParameters; + } +} diff --git a/library/Zend/Di/Definition/ClassDefinition.php b/library/Zend/Di/Definition/ClassDefinition.php new file mode 100755 index 0000000000..2b110659da --- /dev/null +++ b/library/Zend/Di/Definition/ClassDefinition.php @@ -0,0 +1,231 @@ +class = $class; + } + + /** + * @param null|\Callable|array|string $instantiator + * @return self + */ + public function setInstantiator($instantiator) + { + $this->instantiator = $instantiator; + + return $this; + } + + /** + * @param string[] $supertypes + * @return self + */ + public function setSupertypes(array $supertypes) + { + $this->supertypes = $supertypes; + + return $this; + } + + /** + * @param string $method + * @param mixed|bool|null $isRequired + * @return self + */ + public function addMethod($method, $isRequired = null) + { + if ($isRequired === null) { + if ($method === '__construct') { + $methodRequirementType = Di::METHOD_IS_CONSTRUCTOR; + } else { + $methodRequirementType = Di::METHOD_IS_OPTIONAL; + } + } else { + $methodRequirementType = InjectionMethod::detectMethodRequirement($isRequired); + } + + $this->methods[$method] = $methodRequirementType; + + return $this; + } + + /** + * @param $method + * @param $parameterName + * @param array $parameterInfo (keys: required, type) + * @return ClassDefinition + */ + public function addMethodParameter($method, $parameterName, array $parameterInfo) + { + if (!array_key_exists($method, $this->methods)) { + if ($method === '__construct') { + $this->methods[$method] = Di::METHOD_IS_CONSTRUCTOR; + } else { + $this->methods[$method] = Di::METHOD_IS_OPTIONAL; + } + } + + if (!array_key_exists($method, $this->methodParameters)) { + $this->methodParameters[$method] = array(); + } + + $type = (isset($parameterInfo['type'])) ? $parameterInfo['type'] : null; + $required = (isset($parameterInfo['required'])) ? (bool) $parameterInfo['required'] : false; + $default = (isset($parameterInfo['default'])) ? $parameterInfo['default'] : null; + + $fqName = $this->class . '::' . $method . ':' . $parameterName; + $this->methodParameters[$method][$fqName] = array( + $parameterName, + $type, + $required, + $default + ); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getClasses() + { + return array($this->class); + } + + /** + * {@inheritDoc} + */ + public function hasClass($class) + { + return ($class === $this->class); + } + + /** + * {@inheritDoc} + */ + public function getClassSupertypes($class) + { + if ($this->class !== $class) { + return array(); + } + return $this->supertypes; + } + + /** + * {@inheritDoc} + */ + public function getInstantiator($class) + { + if ($this->class !== $class) { + return null; + } + return $this->instantiator; + } + + /** + * {@inheritDoc} + */ + public function hasMethods($class) + { + return (count($this->methods) > 0); + } + + /** + * {@inheritDoc} + */ + public function getMethods($class) + { + if ($this->class !== $class) { + return array(); + } + return $this->methods; + } + + /** + * {@inheritDoc} + */ + public function hasMethod($class, $method) + { + if ($this->class !== $class) { + return null; + } + + if (is_array($this->methods)) { + return array_key_exists($method, $this->methods); + } + + return null; + } + + /** + * {@inheritDoc} + */ + public function hasMethodParameters($class, $method) + { + if ($this->class !== $class) { + return false; + } + return (array_key_exists($method, $this->methodParameters)); + } + + /** + * {@inheritDoc} + */ + public function getMethodParameters($class, $method) + { + if ($this->class !== $class) { + return null; + } + + if (array_key_exists($method, $this->methodParameters)) { + return $this->methodParameters[$method]; + } + + return null; + } +} diff --git a/library/Zend/Di/Definition/CompilerDefinition.php b/library/Zend/Di/Definition/CompilerDefinition.php new file mode 100755 index 0000000000..dcecde10ff --- /dev/null +++ b/library/Zend/Di/Definition/CompilerDefinition.php @@ -0,0 +1,397 @@ +introspectionStrategy = ($introspectionStrategy) ?: new IntrospectionStrategy(); + $this->directoryScanner = new AggregateDirectoryScanner(); + } + + /** + * Set introspection strategy + * + * @param IntrospectionStrategy $introspectionStrategy + */ + public function setIntrospectionStrategy(IntrospectionStrategy $introspectionStrategy) + { + $this->introspectionStrategy = $introspectionStrategy; + } + + /** + * @param bool $allowReflectionExceptions + */ + public function setAllowReflectionExceptions($allowReflectionExceptions = true) + { + $this->allowReflectionExceptions = (bool) $allowReflectionExceptions; + } + + /** + * Get introspection strategy + * + * @return IntrospectionStrategy + */ + public function getIntrospectionStrategy() + { + return $this->introspectionStrategy; + } + + /** + * Add directory + * + * @param string $directory + */ + public function addDirectory($directory) + { + $this->addDirectoryScanner(new DirectoryScanner($directory)); + } + + /** + * Add directory scanner + * + * @param DirectoryScanner $directoryScanner + */ + public function addDirectoryScanner(DirectoryScanner $directoryScanner) + { + $this->directoryScanner->addDirectoryScanner($directoryScanner); + } + + /** + * Add code scanner file + * + * @param FileScanner $fileScanner + */ + public function addCodeScannerFile(FileScanner $fileScanner) + { + if ($this->directoryScanner == null) { + $this->directoryScanner = new DirectoryScanner(); + } + + $this->directoryScanner->addFileScanner($fileScanner); + } + + /** + * Compile + * + * @return void + */ + public function compile() + { + /* @var $classScanner DerivedClassScanner */ + foreach ($this->directoryScanner->getClassNames() as $class) { + $this->processClass($class); + } + } + + /** + * @return ArrayDefinition + */ + public function toArrayDefinition() + { + return new ArrayDefinition( + $this->classes + ); + } + + /** + * @param string $class + * @throws \ReflectionException + */ + protected function processClass($class) + { + $strategy = $this->introspectionStrategy; // localize for readability + + try { + $rClass = new Reflection\ClassReflection($class); + } catch (\ReflectionException $e) { + if (!$this->allowReflectionExceptions) { + throw $e; + } + + return; + } + $className = $rClass->getName(); + $matches = null; // used for regex below + + // setup the key in classes + $this->classes[$className] = array( + 'supertypes' => array(), + 'instantiator' => null, + 'methods' => array(), + 'parameters' => array() + ); + + $def = &$this->classes[$className]; // localize for brevity + + // class annotations? + if ($strategy->getUseAnnotations() == true) { + $annotations = $rClass->getAnnotations($strategy->getAnnotationManager()); + + if (($annotations instanceof AnnotationCollection) + && $annotations->hasAnnotation('Zend\Di\Definition\Annotation\Instantiator') + ) { + // @todo Instantiator support in annotations + } + } + + /* @var $rTarget \Zend\Code\Reflection\ClassReflection */ + $rTarget = $rClass; + $supertypes = array(); + do { + $supertypes = array_merge($supertypes, $rTarget->getInterfaceNames()); + if (!($rTargetParent = $rTarget->getParentClass())) { + break; + } + $supertypes[] = $rTargetParent->getName(); + $rTarget = $rTargetParent; + } while (true); + + $def['supertypes'] = $supertypes; + + if ($def['instantiator'] == null) { + if ($rClass->isInstantiable()) { + $def['instantiator'] = '__construct'; + } + } + + if ($rClass->hasMethod('__construct')) { + $def['methods']['__construct'] = true; // required + try { + $this->processParams($def, $rClass, $rClass->getMethod('__construct')); + } catch (\ReflectionException $e) { + if (!$this->allowReflectionExceptions) { + throw $e; + } + + return; + } + } + + foreach ($rClass->getMethods(Reflection\MethodReflection::IS_PUBLIC) as $rMethod) { + $methodName = $rMethod->getName(); + + if ($rMethod->getName() === '__construct' || $rMethod->isStatic()) { + continue; + } + + if ($strategy->getUseAnnotations() == true) { + $annotations = $rMethod->getAnnotations($strategy->getAnnotationManager()); + + if (($annotations instanceof AnnotationCollection) + && $annotations->hasAnnotation('Zend\Di\Definition\Annotation\Inject') + ) { + $def['methods'][$methodName] = true; + $this->processParams($def, $rClass, $rMethod); + continue; + } + } + + $methodPatterns = $this->introspectionStrategy->getMethodNameInclusionPatterns(); + + // matches a method injection pattern? + foreach ($methodPatterns as $methodInjectorPattern) { + preg_match($methodInjectorPattern, $methodName, $matches); + if ($matches) { + $def['methods'][$methodName] = false; // check ot see if this is required? + $this->processParams($def, $rClass, $rMethod); + continue 2; + } + } + + // method + // by annotation + // by setter pattern, + // by interface + } + + $interfaceInjectorPatterns = $this->introspectionStrategy->getInterfaceInjectionInclusionPatterns(); + + // matches the interface injection pattern + /** @var $rIface \ReflectionClass */ + foreach ($rClass->getInterfaces() as $rIface) { + foreach ($interfaceInjectorPatterns as $interfaceInjectorPattern) { + preg_match($interfaceInjectorPattern, $rIface->getName(), $matches); + if ($matches) { + foreach ($rIface->getMethods() as $rMethod) { + if (($rMethod->getName() === '__construct') || !count($rMethod->getParameters())) { + // constructor not allowed in interfaces + // ignore methods without parameters + continue; + } + $def['methods'][$rMethod->getName()] = true; + $this->processParams($def, $rClass, $rMethod); + } + continue 2; + } + } + } + } + + /** + * @param array $def + * @param \Zend\Code\Reflection\ClassReflection $rClass + * @param \Zend\Code\Reflection\MethodReflection $rMethod + */ + protected function processParams(&$def, Reflection\ClassReflection $rClass, Reflection\MethodReflection $rMethod) + { + if (count($rMethod->getParameters()) === 0) { + return; + } + + $methodName = $rMethod->getName(); + + // @todo annotations here for alternate names? + + $def['parameters'][$methodName] = array(); + + foreach ($rMethod->getParameters() as $p) { + /** @var $p \ReflectionParameter */ + $actualParamName = $p->getName(); + $fqName = $rClass->getName() . '::' . $rMethod->getName() . ':' . $p->getPosition(); + $def['parameters'][$methodName][$fqName] = array(); + + // set the class name, if it exists + $def['parameters'][$methodName][$fqName][] = $actualParamName; + $def['parameters'][$methodName][$fqName][] = ($p->getClass() !== null) ? $p->getClass()->getName() : null; + $def['parameters'][$methodName][$fqName][] = !($optional =$p->isOptional()); + $def['parameters'][$methodName][$fqName][] = $optional && $p->isDefaultValueAvailable() ? $p->getDefaultValue() : null; + } + } + + /** + * {@inheritDoc} + */ + public function getClasses() + { + return array_keys($this->classes); + } + + /** + * {@inheritDoc} + */ + public function hasClass($class) + { + return (array_key_exists($class, $this->classes)); + } + + /** + * {@inheritDoc} + */ + public function getClassSupertypes($class) + { + if (!array_key_exists($class, $this->classes)) { + $this->processClass($class); + } + + return $this->classes[$class]['supertypes']; + } + + /** + * {@inheritDoc} + */ + public function getInstantiator($class) + { + if (!array_key_exists($class, $this->classes)) { + $this->processClass($class); + } + + return $this->classes[$class]['instantiator']; + } + + /** + * {@inheritDoc} + */ + public function hasMethods($class) + { + if (!array_key_exists($class, $this->classes)) { + $this->processClass($class); + } + + return (count($this->classes[$class]['methods']) > 0); + } + + /** + * {@inheritDoc} + */ + public function hasMethod($class, $method) + { + if (!array_key_exists($class, $this->classes)) { + $this->processClass($class); + } + + return isset($this->classes[$class]['methods'][$method]); + } + + /** + * {@inheritDoc} + */ + public function getMethods($class) + { + if (!array_key_exists($class, $this->classes)) { + $this->processClass($class); + } + + return $this->classes[$class]['methods']; + } + + /** + * {@inheritDoc} + */ + public function hasMethodParameters($class, $method) + { + if (!isset($this->classes[$class])) { + return false; + } + + return (array_key_exists($method, $this->classes[$class]['parameters'])); + } + + /** + * {@inheritDoc} + */ + public function getMethodParameters($class, $method) + { + if (!is_array($this->classes[$class])) { + $this->processClass($class); + } + + return $this->classes[$class]['parameters'][$method]; + } +} diff --git a/library/Zend/Di/Definition/DefinitionInterface.php b/library/Zend/Di/Definition/DefinitionInterface.php new file mode 100755 index 0000000000..420bb459d1 --- /dev/null +++ b/library/Zend/Di/Definition/DefinitionInterface.php @@ -0,0 +1,101 @@ +annotationManager = ($annotationManager) ?: $this->createDefaultAnnotationManager(); + } + + /** + * Get annotation manager + * + * @return null|AnnotationManager + */ + public function getAnnotationManager() + { + return $this->annotationManager; + } + + /** + * Create default annotation manager + * + * @return AnnotationManager + */ + public function createDefaultAnnotationManager() + { + $annotationManager = new AnnotationManager; + $parser = new GenericAnnotationParser(); + $parser->registerAnnotation(new Annotation\Inject()); + $annotationManager->attach($parser); + + return $annotationManager; + } + + /** + * set use annotations + * + * @param bool $useAnnotations + */ + public function setUseAnnotations($useAnnotations) + { + $this->useAnnotations = (bool) $useAnnotations; + } + + /** + * Get use annotations + * + * @return bool + */ + public function getUseAnnotations() + { + return $this->useAnnotations; + } + + /** + * Set method name inclusion pattern + * + * @param array $methodNameInclusionPatterns + */ + public function setMethodNameInclusionPatterns(array $methodNameInclusionPatterns) + { + $this->methodNameInclusionPatterns = $methodNameInclusionPatterns; + } + + /** + * Get method name inclusion pattern + * + * @return array + */ + public function getMethodNameInclusionPatterns() + { + return $this->methodNameInclusionPatterns; + } + + /** + * Set interface injection inclusion patterns + * + * @param array $interfaceInjectionInclusionPatterns + */ + public function setInterfaceInjectionInclusionPatterns(array $interfaceInjectionInclusionPatterns) + { + $this->interfaceInjectionInclusionPatterns = $interfaceInjectionInclusionPatterns; + } + + /** + * Get interface injection inclusion patterns + * + * @return array + */ + public function getInterfaceInjectionInclusionPatterns() + { + return $this->interfaceInjectionInclusionPatterns; + } +} diff --git a/library/Zend/Di/Definition/PartialMarker.php b/library/Zend/Di/Definition/PartialMarker.php new file mode 100755 index 0000000000..4a40728f0b --- /dev/null +++ b/library/Zend/Di/Definition/PartialMarker.php @@ -0,0 +1,14 @@ +introspectionStrategy = ($introspectionStrategy) ?: new IntrospectionStrategy(); + if ($explicitClasses) { + $this->setExplicitClasses($explicitClasses); + } + } + + /** + * @param IntrospectionStrategy $introspectionStrategy + * @return void + */ + public function setIntrospectionStrategy(IntrospectionStrategy $introspectionStrategy) + { + $this->introspectionStrategy = $introspectionStrategy; + } + + /** + * @return IntrospectionStrategy + */ + public function getIntrospectionStrategy() + { + return $this->introspectionStrategy; + } + + /** + * Set explicit classes + * + * @param array $explicitClasses + */ + public function setExplicitClasses(array $explicitClasses) + { + $this->explicitLookups = true; + foreach ($explicitClasses as $eClass) { + $this->classes[$eClass] = true; + } + $this->classes = $explicitClasses; + } + + /** + * @param string $class + */ + public function forceLoadClass($class) + { + $this->processClass($class, true); + } + + /** + * {@inheritDoc} + */ + public function getClasses() + { + return array_keys($this->classes); + } + + /** + * {@inheritDoc} + */ + public function hasClass($class) + { + if ($this->explicitLookups === true) { + return (array_key_exists($class, $this->classes)); + } + + return class_exists($class) || interface_exists($class); + } + + /** + * {@inheritDoc} + */ + public function getClassSupertypes($class) + { + $this->processClass($class); + return $this->classes[$class]['supertypes']; + } + + /** + * {@inheritDoc} + */ + public function getInstantiator($class) + { + $this->processClass($class); + return $this->classes[$class]['instantiator']; + } + + /** + * {@inheritDoc} + */ + public function hasMethods($class) + { + $this->processClass($class); + return (count($this->classes[$class]['methods']) > 0); + } + + /** + * {@inheritDoc} + */ + public function hasMethod($class, $method) + { + $this->processClass($class); + return isset($this->classes[$class]['methods'][$method]); + } + + /** + * {@inheritDoc} + */ + public function getMethods($class) + { + $this->processClass($class); + return $this->classes[$class]['methods']; + } + + /** + * {@inheritDoc} + */ + public function hasMethodParameters($class, $method) + { + $this->processClass($class); + return (array_key_exists($method, $this->classes[$class]['parameters'])); + } + + /** + * {@inheritDoc} + */ + public function getMethodParameters($class, $method) + { + $this->processClass($class); + return $this->classes[$class]['parameters'][$method]; + } + + /** + * @param string $class + * + * @return bool + */ + protected function hasProcessedClass($class) + { + return array_key_exists($class, $this->classes) && is_array($this->classes[$class]); + } + + /** + * @param string $class + * @param bool $forceLoad + */ + protected function processClass($class, $forceLoad = false) + { + if (!$forceLoad && $this->hasProcessedClass($class)) { + return; + } + $strategy = $this->introspectionStrategy; // localize for readability + + /** @var $rClass \Zend\Code\Reflection\ClassReflection */ + $rClass = new Reflection\ClassReflection($class); + $className = $rClass->getName(); + $matches = null; // used for regex below + + // setup the key in classes + $this->classes[$className] = array( + 'supertypes' => array(), + 'instantiator' => null, + 'methods' => array(), + 'parameters' => array() + ); + + $def = &$this->classes[$className]; // localize for brevity + + // class annotations? + if ($strategy->getUseAnnotations() == true) { + $annotations = $rClass->getAnnotations($strategy->getAnnotationManager()); + + if (($annotations instanceof AnnotationCollection) + && $annotations->hasAnnotation('Zend\Di\Definition\Annotation\Instantiator')) { + // @todo Instantiator support in annotations + } + } + + $rTarget = $rClass; + $supertypes = array(); + do { + $supertypes = array_merge($supertypes, $rTarget->getInterfaceNames()); + if (!($rTargetParent = $rTarget->getParentClass())) { + break; + } + $supertypes[] = $rTargetParent->getName(); + $rTarget = $rTargetParent; + } while (true); + + $def['supertypes'] = $supertypes; + + if ($def['instantiator'] == null) { + if ($rClass->isInstantiable()) { + $def['instantiator'] = '__construct'; + } + } + + if ($rClass->hasMethod('__construct')) { + $def['methods']['__construct'] = Di::METHOD_IS_CONSTRUCTOR; // required + $this->processParams($def, $rClass, $rClass->getMethod('__construct')); + } + + foreach ($rClass->getMethods(Reflection\MethodReflection::IS_PUBLIC) as $rMethod) { + $methodName = $rMethod->getName(); + + if ($rMethod->getName() === '__construct' || $rMethod->isStatic()) { + continue; + } + + if ($strategy->getUseAnnotations() == true) { + $annotations = $rMethod->getAnnotations($strategy->getAnnotationManager()); + + if (($annotations instanceof AnnotationCollection) + && $annotations->hasAnnotation('Zend\Di\Definition\Annotation\Inject')) { + // use '@inject' and search for parameters + $def['methods'][$methodName] = Di::METHOD_IS_EAGER; + $this->processParams($def, $rClass, $rMethod); + continue; + } + } + + $methodPatterns = $this->introspectionStrategy->getMethodNameInclusionPatterns(); + + // matches a method injection pattern? + foreach ($methodPatterns as $methodInjectorPattern) { + preg_match($methodInjectorPattern, $methodName, $matches); + if ($matches) { + $def['methods'][$methodName] = Di::METHOD_IS_OPTIONAL; // check ot see if this is required? + $this->processParams($def, $rClass, $rMethod); + continue 2; + } + } + + // method + // by annotation + // by setter pattern, + // by interface + } + + $interfaceInjectorPatterns = $this->introspectionStrategy->getInterfaceInjectionInclusionPatterns(); + + // matches the interface injection pattern + /** @var $rIface \ReflectionClass */ + foreach ($rClass->getInterfaces() as $rIface) { + foreach ($interfaceInjectorPatterns as $interfaceInjectorPattern) { + preg_match($interfaceInjectorPattern, $rIface->getName(), $matches); + if ($matches) { + foreach ($rIface->getMethods() as $rMethod) { + if (($rMethod->getName() === '__construct') || !count($rMethod->getParameters())) { + // constructor not allowed in interfaces + // Don't call interface methods without a parameter (Some aware interfaces define setters in ZF2) + continue; + } + $def['methods'][$rMethod->getName()] = Di::METHOD_IS_AWARE; + $this->processParams($def, $rClass, $rMethod); + } + continue 2; + } + } + } + } + + /** + * @param array $def + * @param \Zend\Code\Reflection\ClassReflection $rClass + * @param \Zend\Code\Reflection\MethodReflection $rMethod + */ + protected function processParams(&$def, Reflection\ClassReflection $rClass, Reflection\MethodReflection $rMethod) + { + if (count($rMethod->getParameters()) === 0) { + return; + } + + $methodName = $rMethod->getName(); + + // @todo annotations here for alternate names? + + $def['parameters'][$methodName] = array(); + + foreach ($rMethod->getParameters() as $p) { + /** @var $p \ReflectionParameter */ + $actualParamName = $p->getName(); + + $fqName = $rClass->getName() . '::' . $rMethod->getName() . ':' . $p->getPosition(); + + $def['parameters'][$methodName][$fqName] = array(); + + // set the class name, if it exists + $def['parameters'][$methodName][$fqName][] = $actualParamName; + $def['parameters'][$methodName][$fqName][] = ($p->getClass() !== null) ? $p->getClass()->getName() : null; + $def['parameters'][$methodName][$fqName][] = !($optional = $p->isOptional() && $p->isDefaultValueAvailable()); + $def['parameters'][$methodName][$fqName][] = $optional ? $p->getDefaultValue() : null; + } + } +} diff --git a/library/Zend/Di/DefinitionList.php b/library/Zend/Di/DefinitionList.php new file mode 100755 index 0000000000..efe190e57f --- /dev/null +++ b/library/Zend/Di/DefinitionList.php @@ -0,0 +1,257 @@ +push($definition); + } + } + + /** + * Add definitions + * + * @param Definition\DefinitionInterface $definition + * @param bool $addToBackOfList + * @return void + */ + public function addDefinition(Definition\DefinitionInterface $definition, $addToBackOfList = true) + { + if ($addToBackOfList) { + $this->push($definition); + } else { + $this->unshift($definition); + } + } + + /** + * @param string $type + * @return Definition\DefinitionInterface[] + */ + public function getDefinitionsByType($type) + { + $definitions = array(); + foreach ($this as $definition) { + if ($definition instanceof $type) { + $definitions[] = $definition; + } + } + + return $definitions; + } + + /** + * Get definition by type + * + * @param string $type + * @return Definition\DefinitionInterface + */ + public function getDefinitionByType($type) + { + foreach ($this as $definition) { + if ($definition instanceof $type) { + return $definition; + } + } + + return false; + } + + /** + * @param string $class + * @return bool|Definition\DefinitionInterface + */ + public function getDefinitionForClass($class) + { + /** @var $definition Definition\DefinitionInterface */ + foreach ($this as $definition) { + if ($definition->hasClass($class)) { + return $definition; + } + } + + return false; + } + + /** + * @param string $class + * @return bool|Definition\DefinitionInterface + */ + public function forClass($class) + { + return $this->getDefinitionForClass($class); + } + + /** + * {@inheritDoc} + */ + public function getClasses() + { + $classes = array(); + /** @var $definition Definition\DefinitionInterface */ + foreach ($this as $definition) { + $classes = array_merge($classes, $definition->getClasses()); + } + + return $classes; + } + + /** + * {@inheritDoc} + */ + public function hasClass($class) + { + /** @var $definition Definition\DefinitionInterface */ + foreach ($this as $definition) { + if ($definition->hasClass($class)) { + return true; + } + } + + return false; + } + + /** + * {@inheritDoc} + */ + public function getClassSupertypes($class) + { + $supertypes = array(); + /** @var $definition Definition\DefinitionInterface */ + foreach ($this as $definition) { + if ($definition->hasClass($class)) { + $supertypes = array_merge($supertypes, $definition->getClassSupertypes($class)); + if ($definition instanceof Definition\PartialMarker) { + continue; + } + + return $supertypes; + } + } + return $supertypes; + } + + /** + * {@inheritDoc} + */ + public function getInstantiator($class) + { + /** @var $definition Definition\DefinitionInterface */ + foreach ($this as $definition) { + if ($definition->hasClass($class)) { + $value = $definition->getInstantiator($class); + if ($value === null && $definition instanceof Definition\PartialMarker) { + continue; + } + + return $value; + } + } + + return false; + } + + /** + * {@inheritDoc} + */ + public function hasMethods($class) + { + /** @var $definition Definition\DefinitionInterface */ + foreach ($this as $definition) { + if ($definition->hasClass($class)) { + if ($definition->hasMethods($class) === false && $definition instanceof Definition\PartialMarker) { + continue; + } + + return $definition->hasMethods($class); + } + } + + return false; + } + + /** + * {@inheritDoc} + */ + public function hasMethod($class, $method) + { + if (!$this->hasMethods($class)) { + return false; + } + + /** @var $definition Definition\DefinitionInterface */ + foreach ($this as $definition) { + if ($definition->hasClass($class) && $definition->hasMethod($class, $method)) { + return true; + } + } + + return false; + } + + /** + * {@inheritDoc} + */ + public function getMethods($class) + { + /** @var $definition Definition\DefinitionInterface */ + $methods = array(); + foreach ($this as $definition) { + if ($definition->hasClass($class)) { + if (!$definition instanceof Definition\PartialMarker) { + return array_merge($definition->getMethods($class), $methods); + } + + $methods = array_merge($definition->getMethods($class), $methods); + } + } + + return $methods; + } + + /** + * {@inheritDoc} + */ + public function hasMethodParameters($class, $method) + { + $methodParameters = $this->getMethodParameters($class, $method); + + return ($methodParameters !== array()); + } + + /** + * {@inheritDoc} + */ + public function getMethodParameters($class, $method) + { + /** @var $definition Definition\DefinitionInterface */ + foreach ($this as $definition) { + if ($definition->hasClass($class) && $definition->hasMethod($class, $method) && $definition->hasMethodParameters($class, $method)) { + return $definition->getMethodParameters($class, $method); + } + } + + return array(); + } +} diff --git a/library/Zend/Di/DependencyInjectionInterface.php b/library/Zend/Di/DependencyInjectionInterface.php new file mode 100755 index 0000000000..b821876136 --- /dev/null +++ b/library/Zend/Di/DependencyInjectionInterface.php @@ -0,0 +1,25 @@ +definitions = ($definitions) ?: new DefinitionList(new Definition\RuntimeDefinition()); + $this->instanceManager = ($instanceManager) ?: new InstanceManager(); + + if ($config) { + $this->configure($config); + } + } + + /** + * Provide a configuration object to configure this instance + * + * @param Config $config + * @return void + */ + public function configure(Config $config) + { + $config->configure($this); + } + + /** + * @param DefinitionList $definitions + * @return self + */ + public function setDefinitionList(DefinitionList $definitions) + { + $this->definitions = $definitions; + + return $this; + } + + /** + * @return DefinitionList + */ + public function definitions() + { + return $this->definitions; + } + + /** + * Set the instance manager + * + * @param InstanceManager $instanceManager + * @return Di + */ + public function setInstanceManager(InstanceManager $instanceManager) + { + $this->instanceManager = $instanceManager; + + return $this; + } + + /** + * + * @return InstanceManager + */ + public function instanceManager() + { + return $this->instanceManager; + } + + /** + * Utility method used to retrieve the class of a particular instance. This is here to allow extending classes to + * override how class names are resolved + * + * @internal this method is used by the ServiceLocator\DependencyInjectorProxy class to interact with instances + * and is a hack to be used internally until a major refactor does not split the `resolveMethodParameters`. Do not + * rely on its functionality. + * @param object $instance + * @return string + */ + protected function getClass($instance) + { + return get_class($instance); + } + + /** + * @param $name + * @param array $params + * @param string $method + * @return array + */ + protected function getCallParameters($name, array $params, $method = "__construct") + { + $im = $this->instanceManager; + $class = $im->hasAlias($name) ? $im->getClassFromAlias($name) : $name; + if ($this->definitions->hasClass($class)) { + $callParameters = array(); + if ($this->definitions->hasMethod($class, $method)) { + foreach ($this->definitions->getMethodParameters($class, $method) as $param) { + if (isset($params[$param[0]])) { + $callParameters[$param[0]] = $params[$param[0]]; + } + } + } + return $callParameters; + } + return $params; + } + + /** + * Lazy-load a class + * + * Attempts to load the class (or service alias) provided. If it has been + * loaded before, the previous instance will be returned (unless the service + * definition indicates shared instances should not be used). + * + * @param string $name Class name or service alias + * @param null|array $params Parameters to pass to the constructor + * @return object|null + */ + public function get($name, array $params = array()) + { + array_push($this->instanceContext, array('GET', $name, null)); + + $im = $this->instanceManager; + + $callParameters = $this->getCallParameters($name, $params); + if ($callParameters) { + $fastHash = $im->hasSharedInstanceWithParameters($name, $callParameters, true); + if ($fastHash) { + array_pop($this->instanceContext); + return $im->getSharedInstanceWithParameters(null, array(), $fastHash); + } + } + + if ($im->hasSharedInstance($name, $callParameters)) { + array_pop($this->instanceContext); + return $im->getSharedInstance($name, $callParameters); + } + + + $config = $im->getConfig($name); + $instance = $this->newInstance($name, $params, $config['shared']); + array_pop($this->instanceContext); + + return $instance; + } + + /** + * Retrieve a new instance of a class + * + * Forces retrieval of a discrete instance of the given class, using the + * constructor parameters provided. + * + * @param mixed $name Class name or service alias + * @param array $params Parameters to pass to the constructor + * @param bool $isShared + * @return object|null + * @throws Exception\ClassNotFoundException + * @throws Exception\RuntimeException + */ + public function newInstance($name, array $params = array(), $isShared = true) + { + // localize dependencies + $definitions = $this->definitions; + $instanceManager = $this->instanceManager(); + + if ($instanceManager->hasAlias($name)) { + $class = $instanceManager->getClassFromAlias($name); + $alias = $name; + } else { + $class = $name; + $alias = null; + } + + array_push($this->instanceContext, array('NEW', $class, $alias)); + + if (!$definitions->hasClass($class)) { + $aliasMsg = ($alias) ? '(specified by alias ' . $alias . ') ' : ''; + throw new Exception\ClassNotFoundException( + 'Class ' . $aliasMsg . $class . ' could not be located in provided definitions.' + ); + } + + $instantiator = $definitions->getInstantiator($class); + $injectionMethods = array(); + $injectionMethods[$class] = $definitions->getMethods($class); + + foreach ($definitions->getClassSupertypes($class) as $supertype) { + $injectionMethods[$supertype] = $definitions->getMethods($supertype); + } + + if ($instantiator === '__construct') { + $instance = $this->createInstanceViaConstructor($class, $params, $alias); + if (array_key_exists('__construct', $injectionMethods)) { + unset($injectionMethods['__construct']); + } + } elseif (is_callable($instantiator, false)) { + $instance = $this->createInstanceViaCallback($instantiator, $params, $alias); + } else { + if (is_array($instantiator)) { + $msg = sprintf( + 'Invalid instantiator: %s::%s() is not callable.', + isset($instantiator[0]) ? $instantiator[0] : 'NoClassGiven', + isset($instantiator[1]) ? $instantiator[1] : 'NoMethodGiven' + ); + } else { + $msg = sprintf( + 'Invalid instantiator of type "%s" for "%s".', + gettype($instantiator), + $name + ); + } + throw new Exception\RuntimeException($msg); + } + + if ($isShared) { + if ($callParameters = $this->getCallParameters($name, $params)) { + $this->instanceManager->addSharedInstanceWithParameters($instance, $name, $callParameters); + } else { + $this->instanceManager->addSharedInstance($instance, $name); + } + } + + $this->handleInjectDependencies($instance, $injectionMethods, $params, $class, $alias, $name); + + array_pop($this->instanceContext); + + return $instance; + } + + /** + * Inject dependencies + * + * @param object $instance + * @param array $params + * @return void + */ + public function injectDependencies($instance, array $params = array()) + { + $definitions = $this->definitions(); + $class = $this->getClass($instance); + $injectionMethods = array( + $class => ($definitions->hasClass($class)) ? $definitions->getMethods($class) : array() + ); + $parent = $class; + while ($parent = get_parent_class($parent)) { + if ($definitions->hasClass($parent)) { + $injectionMethods[$parent] = $definitions->getMethods($parent); + } + } + foreach (class_implements($class) as $interface) { + if ($definitions->hasClass($interface)) { + $injectionMethods[$interface] = $definitions->getMethods($interface); + } + } + $this->handleInjectDependencies($instance, $injectionMethods, $params, $class, null, null); + } + + /** + * @param object $instance + * @param array $injectionMethods + * @param array $params + * @param string|null $instanceClass + * @param string|null$instanceAlias + * @param string $requestedName + * @throws Exception\RuntimeException + */ + protected function handleInjectDependencies($instance, $injectionMethods, $params, $instanceClass, $instanceAlias, $requestedName) + { + // localize dependencies + $definitions = $this->definitions; + $instanceManager = $this->instanceManager(); + + $calledMethods = array('__construct' => true); + + if ($injectionMethods) { + foreach ($injectionMethods as $type => $typeInjectionMethods) { + foreach ($typeInjectionMethods as $typeInjectionMethod => $methodRequirementType) { + if (!isset($calledMethods[$typeInjectionMethod])) { + if ($this->resolveAndCallInjectionMethodForInstance($instance, $typeInjectionMethod, $params, $instanceAlias, $methodRequirementType, $type)) { + $calledMethods[$typeInjectionMethod] = true; + } + } + } + } + + if ($requestedName) { + $instanceConfig = $instanceManager->getConfig($requestedName); + + if ($instanceConfig['injections']) { + $objectsToInject = $methodsToCall = array(); + foreach ($instanceConfig['injections'] as $injectName => $injectValue) { + if (is_int($injectName) && is_string($injectValue)) { + $objectsToInject[] = $this->get($injectValue, $params); + } elseif (is_string($injectName) && is_array($injectValue)) { + if (is_string(key($injectValue))) { + $methodsToCall[] = array('method' => $injectName, 'args' => $injectValue); + } else { + foreach ($injectValue as $methodCallArgs) { + $methodsToCall[] = array('method' => $injectName, 'args' => $methodCallArgs); + } + } + } elseif (is_object($injectValue)) { + $objectsToInject[] = $injectValue; + } elseif (is_int($injectName) && is_array($injectValue)) { + throw new Exception\RuntimeException( + 'An injection was provided with a keyed index and an array of data, try using' + . ' the name of a particular method as a key for your injection data.' + ); + } + } + if ($objectsToInject) { + foreach ($objectsToInject as $objectToInject) { + $calledMethods = array('__construct' => true); + foreach ($injectionMethods as $type => $typeInjectionMethods) { + foreach ($typeInjectionMethods as $typeInjectionMethod => $methodRequirementType) { + if (!isset($calledMethods[$typeInjectionMethod])) { + $methodParams = $definitions->getMethodParameters($type, $typeInjectionMethod); + if ($methodParams) { + foreach ($methodParams as $methodParam) { + $objectToInjectClass = $this->getClass($objectToInject); + if ($objectToInjectClass == $methodParam[1] || self::isSubclassOf($objectToInjectClass, $methodParam[1])) { + if ($this->resolveAndCallInjectionMethodForInstance($instance, $typeInjectionMethod, array($methodParam[0] => $objectToInject), $instanceAlias, self::METHOD_IS_REQUIRED, $type)) { + $calledMethods[$typeInjectionMethod] = true; + } + continue 3; + } + } + } + } + } + } + } + } + if ($methodsToCall) { + foreach ($methodsToCall as $methodInfo) { + $this->resolveAndCallInjectionMethodForInstance($instance, $methodInfo['method'], $methodInfo['args'], $instanceAlias, self::METHOD_IS_REQUIRED, $instanceClass); + } + } + } + } + } + } + + /** + * Retrieve a class instance based on class name + * + * Any parameters provided will be used as constructor arguments. If any + * given parameter is a DependencyReference object, it will be fetched + * from the container so that the instance may be injected. + * + * @param string $class + * @param array $params + * @param string|null $alias + * @return object + */ + protected function createInstanceViaConstructor($class, $params, $alias = null) + { + $callParameters = array(); + if ($this->definitions->hasMethod($class, '__construct')) { + $callParameters = $this->resolveMethodParameters($class, '__construct', $params, $alias, self::METHOD_IS_CONSTRUCTOR, true); + } + + if (!class_exists($class)) { + if (interface_exists($class)) { + throw new Exception\ClassNotFoundException(sprintf( + 'Cannot instantiate interface "%s"', + $class + )); + } + throw new Exception\ClassNotFoundException(sprintf( + 'Class "%s" does not exist; cannot instantiate', + $class + )); + } + + // Hack to avoid Reflection in most common use cases + switch (count($callParameters)) { + case 0: + return new $class(); + case 1: + return new $class($callParameters[0]); + case 2: + return new $class($callParameters[0], $callParameters[1]); + case 3: + return new $class($callParameters[0], $callParameters[1], $callParameters[2]); + default: + $r = new \ReflectionClass($class); + + return $r->newInstanceArgs($callParameters); + } + } + + /** + * Get an object instance from the defined callback + * + * @param callable $callback + * @param array $params + * @param string $alias + * @return object + * @throws Exception\InvalidCallbackException + * @throws Exception\RuntimeException + */ + protected function createInstanceViaCallback($callback, $params, $alias) + { + if (!is_callable($callback)) { + throw new Exception\InvalidCallbackException('An invalid constructor callback was provided'); + } + + if (is_array($callback)) { + $class = (is_object($callback[0])) ? $this->getClass($callback[0]) : $callback[0]; + $method = $callback[1]; + } elseif (is_string($callback) && strpos($callback, '::') !== false) { + list($class, $method) = explode('::', $callback, 2); + } else { + throw new Exception\RuntimeException('Invalid callback type provided to ' . __METHOD__); + } + + $callParameters = array(); + if ($this->definitions->hasMethod($class, $method)) { + $callParameters = $this->resolveMethodParameters($class, $method, $params, $alias, self::METHOD_IS_INSTANTIATOR, true); + } + + return call_user_func_array($callback, $callParameters); + } + + /** + * This parameter will handle any injection methods and resolution of + * dependencies for such methods + * + * @param object $instance + * @param string $method + * @param array $params + * @param string $alias + * @param bool $methodRequirementType + * @param string|null $methodClass + * @return bool + */ + protected function resolveAndCallInjectionMethodForInstance($instance, $method, $params, $alias, $methodRequirementType, $methodClass = null) + { + $methodClass = ($methodClass) ?: $this->getClass($instance); + $callParameters = $this->resolveMethodParameters($methodClass, $method, $params, $alias, $methodRequirementType); + if ($callParameters == false) { + return false; + } + if ($callParameters !== array_fill(0, count($callParameters), null)) { + call_user_func_array(array($instance, $method), $callParameters); + + return true; + } + + return false; + } + + /** + * Resolve parameters referencing other services + * + * @param string $class + * @param string $method + * @param array $callTimeUserParams + * @param string $alias + * @param int|bool $methodRequirementType + * @param bool $isInstantiator + * @throws Exception\MissingPropertyException + * @throws Exception\CircularDependencyException + * @return array + */ + protected function resolveMethodParameters($class, $method, array $callTimeUserParams, $alias, $methodRequirementType, $isInstantiator = false) + { + //for BC + if (is_bool($methodRequirementType)) { + if ($isInstantiator) { + $methodRequirementType = Di::METHOD_IS_INSTANTIATOR; + } elseif ($methodRequirementType) { + $methodRequirementType = Di::METHOD_IS_REQUIRED; + } else { + $methodRequirementType = Di::METHOD_IS_OPTIONAL; + } + } + // parameters for this method, in proper order, to be returned + $resolvedParams = array(); + + // parameter requirements from the definition + $injectionMethodParameters = $this->definitions->getMethodParameters($class, $method); + + // computed parameters array + $computedParams = array( + 'value' => array(), + 'retrieval' => array(), + 'optional' => array() + ); + + // retrieve instance configurations for all contexts + $iConfig = array(); + $aliases = $this->instanceManager->getAliases(); + + // for the alias in the dependency tree + if ($alias && $this->instanceManager->hasConfig($alias)) { + $iConfig['thisAlias'] = $this->instanceManager->getConfig($alias); + } + + // for the current class in the dependency tree + if ($this->instanceManager->hasConfig($class)) { + $iConfig['thisClass'] = $this->instanceManager->getConfig($class); + } + + // for the parent class, provided we are deeper than one node + if (isset($this->instanceContext[0])) { + list($requestedClass, $requestedAlias) = ($this->instanceContext[0][0] == 'NEW') + ? array($this->instanceContext[0][1], $this->instanceContext[0][2]) + : array($this->instanceContext[1][1], $this->instanceContext[1][2]); + } else { + $requestedClass = $requestedAlias = null; + } + + if ($requestedClass != $class && $this->instanceManager->hasConfig($requestedClass)) { + $iConfig['requestedClass'] = $this->instanceManager->getConfig($requestedClass); + + if (array_key_exists('parameters', $iConfig['requestedClass'])) { + $newParameters = array(); + + foreach ($iConfig['requestedClass']['parameters'] as $name=>$parameter) { + $newParameters[$requestedClass.'::'.$method.'::'.$name] = $parameter; + } + + $iConfig['requestedClass']['parameters'] = $newParameters; + } + + if ($requestedAlias) { + $iConfig['requestedAlias'] = $this->instanceManager->getConfig($requestedAlias); + } + } + + // This is a 2 pass system for resolving parameters + // first pass will find the sources, the second pass will order them and resolve lookups if they exist + // MOST methods will only have a single parameters to resolve, so this should be fast + + foreach ($injectionMethodParameters as $fqParamPos => $info) { + list($name, $type, $isRequired) = $info; + + $fqParamName = substr_replace($fqParamPos, ':' . $info[0], strrpos($fqParamPos, ':')); + + // PRIORITY 1 - consult user provided parameters + if (isset($callTimeUserParams[$fqParamPos]) || isset($callTimeUserParams[$name])) { + if (isset($callTimeUserParams[$fqParamPos])) { + $callTimeCurValue =& $callTimeUserParams[$fqParamPos]; + } elseif (isset($callTimeUserParams[$fqParamName])) { + $callTimeCurValue =& $callTimeUserParams[$fqParamName]; + } else { + $callTimeCurValue =& $callTimeUserParams[$name]; + } + + if ($type !== false && is_string($callTimeCurValue)) { + if ($this->instanceManager->hasAlias($callTimeCurValue)) { + // was an alias provided? + $computedParams['retrieval'][$fqParamPos] = array( + $callTimeUserParams[$name], + $this->instanceManager->getClassFromAlias($callTimeCurValue) + ); + } elseif ($this->definitions->hasClass($callTimeUserParams[$name])) { + // was a known class provided? + $computedParams['retrieval'][$fqParamPos] = array( + $callTimeCurValue, + $callTimeCurValue + ); + } else { + // must be a value + $computedParams['value'][$fqParamPos] = $callTimeCurValue; + } + } else { + // int, float, null, object, etc + $computedParams['value'][$fqParamPos] = $callTimeCurValue; + } + unset($callTimeCurValue); + continue; + } + + // PRIORITY 2 -specific instance configuration (thisAlias) - this alias + // PRIORITY 3 -THEN specific instance configuration (thisClass) - this class + // PRIORITY 4 -THEN specific instance configuration (requestedAlias) - requested alias + // PRIORITY 5 -THEN specific instance configuration (requestedClass) - requested class + + foreach (array('thisAlias', 'thisClass', 'requestedAlias', 'requestedClass') as $thisIndex) { + // check the provided parameters config + if (isset($iConfig[$thisIndex]['parameters'][$fqParamPos]) + || isset($iConfig[$thisIndex]['parameters'][$fqParamName]) + || isset($iConfig[$thisIndex]['parameters'][$name])) { + if (isset($iConfig[$thisIndex]['parameters'][$fqParamPos])) { + $iConfigCurValue =& $iConfig[$thisIndex]['parameters'][$fqParamPos]; + } elseif (isset($iConfig[$thisIndex]['parameters'][$fqParamName])) { + $iConfigCurValue =& $iConfig[$thisIndex]['parameters'][$fqParamName]; + } else { + $iConfigCurValue =& $iConfig[$thisIndex]['parameters'][$name]; + } + + if ($type === false && is_string($iConfigCurValue)) { + $computedParams['value'][$fqParamPos] = $iConfigCurValue; + } elseif (is_string($iConfigCurValue) + && isset($aliases[$iConfigCurValue])) { + $computedParams['retrieval'][$fqParamPos] = array( + $iConfig[$thisIndex]['parameters'][$name], + $this->instanceManager->getClassFromAlias($iConfigCurValue) + ); + } elseif (is_string($iConfigCurValue) + && $this->definitions->hasClass($iConfigCurValue)) { + $computedParams['retrieval'][$fqParamPos] = array( + $iConfigCurValue, + $iConfigCurValue + ); + } elseif (is_object($iConfigCurValue) + && $iConfigCurValue instanceof Closure + && $type !== 'Closure') { + /* @var $iConfigCurValue Closure */ + $computedParams['value'][$fqParamPos] = $iConfigCurValue(); + } else { + $computedParams['value'][$fqParamPos] = $iConfigCurValue; + } + unset($iConfigCurValue); + continue 2; + } + } + + // PRIORITY 6 - globally preferred implementations + + // next consult alias level preferred instances + // RESOLVE_EAGER wants to inject the cross-cutting concerns. + // If you want to retrieve an instance from TypePreferences, + // use AwareInterface or specify the method requirement option METHOD_IS_EAGER at ClassDefinition + if ($methodRequirementType & self::RESOLVE_EAGER) { + if ($alias && $this->instanceManager->hasTypePreferences($alias)) { + $pInstances = $this->instanceManager->getTypePreferences($alias); + foreach ($pInstances as $pInstance) { + if (is_object($pInstance)) { + $computedParams['value'][$fqParamPos] = $pInstance; + continue 2; + } + $pInstanceClass = ($this->instanceManager->hasAlias($pInstance)) ? + $this->instanceManager->getClassFromAlias($pInstance) : $pInstance; + if ($pInstanceClass === $type || self::isSubclassOf($pInstanceClass, $type)) { + $computedParams['retrieval'][$fqParamPos] = array($pInstance, $pInstanceClass); + continue 2; + } + } + } + + // next consult class level preferred instances + if ($type && $this->instanceManager->hasTypePreferences($type)) { + $pInstances = $this->instanceManager->getTypePreferences($type); + foreach ($pInstances as $pInstance) { + if (is_object($pInstance)) { + $computedParams['value'][$fqParamPos] = $pInstance; + continue 2; + } + $pInstanceClass = ($this->instanceManager->hasAlias($pInstance)) ? + $this->instanceManager->getClassFromAlias($pInstance) : $pInstance; + if ($pInstanceClass === $type || self::isSubclassOf($pInstanceClass, $type)) { + $computedParams['retrieval'][$fqParamPos] = array($pInstance, $pInstanceClass); + continue 2; + } + } + } + } + if (!$isRequired) { + $computedParams['optional'][$fqParamPos] = true; + } + + if ($type && $isRequired && ($methodRequirementType & self::RESOLVE_EAGER)) { + $computedParams['retrieval'][$fqParamPos] = array($type, $type); + } + } + + $index = 0; + foreach ($injectionMethodParameters as $fqParamPos => $value) { + $name = $value[0]; + + if (isset($computedParams['value'][$fqParamPos])) { + // if there is a value supplied, use it + $resolvedParams[$index] = $computedParams['value'][$fqParamPos]; + } elseif (isset($computedParams['retrieval'][$fqParamPos])) { + // detect circular dependencies! (they can only happen in instantiators) + if ($isInstantiator && in_array($computedParams['retrieval'][$fqParamPos][1], $this->currentDependencies) + && (!isset($alias) || in_array($computedParams['retrieval'][$fqParamPos][0], $this->currentAliasDependenencies)) + ) { + $msg = "Circular dependency detected: $class depends on {$value[1]} and viceversa"; + if (isset($alias)) { + $msg .= " (Aliased as $alias)"; + } + throw new Exception\CircularDependencyException($msg); + } + + array_push($this->currentDependencies, $class); + if (isset($alias)) { + array_push($this->currentAliasDependenencies, $alias); + } + + $dConfig = $this->instanceManager->getConfig($computedParams['retrieval'][$fqParamPos][0]); + + try { + if ($dConfig['shared'] === false) { + $resolvedParams[$index] = $this->newInstance($computedParams['retrieval'][$fqParamPos][0], $callTimeUserParams, false); + } else { + $resolvedParams[$index] = $this->get($computedParams['retrieval'][$fqParamPos][0], $callTimeUserParams); + } + } catch (DiRuntimeException $e) { + if ($methodRequirementType & self::RESOLVE_STRICT) { + //finally ( be aware to do at the end of flow) + array_pop($this->currentDependencies); + if (isset($alias)) { + array_pop($this->currentAliasDependenencies); + } + // if this item was marked strict, + // plus it cannot be resolve, and no value exist, bail out + throw new Exception\MissingPropertyException(sprintf( + 'Missing %s for parameter ' . $name . ' for ' . $class . '::' . $method, + (($value[0] === null) ? 'value' : 'instance/object' ) + ), + $e->getCode(), + $e); + } else { + //finally ( be aware to do at the end of flow) + array_pop($this->currentDependencies); + if (isset($alias)) { + array_pop($this->currentAliasDependenencies); + } + return false; + } + } catch (ServiceManagerException $e) { + // Zend\ServiceManager\Exception\ServiceNotCreatedException + if ($methodRequirementType & self::RESOLVE_STRICT) { + //finally ( be aware to do at the end of flow) + array_pop($this->currentDependencies); + if (isset($alias)) { + array_pop($this->currentAliasDependenencies); + } + // if this item was marked strict, + // plus it cannot be resolve, and no value exist, bail out + throw new Exception\MissingPropertyException(sprintf( + 'Missing %s for parameter ' . $name . ' for ' . $class . '::' . $method, + (($value[0] === null) ? 'value' : 'instance/object' ) + ), + $e->getCode(), + $e); + } else { + //finally ( be aware to do at the end of flow) + array_pop($this->currentDependencies); + if (isset($alias)) { + array_pop($this->currentAliasDependenencies); + } + return false; + } + } + array_pop($this->currentDependencies); + if (isset($alias)) { + array_pop($this->currentAliasDependenencies); + } + } elseif (!array_key_exists($fqParamPos, $computedParams['optional'])) { + if ($methodRequirementType & self::RESOLVE_STRICT) { + // if this item was not marked as optional, + // plus it cannot be resolve, and no value exist, bail out + throw new Exception\MissingPropertyException(sprintf( + 'Missing %s for parameter ' . $name . ' for ' . $class . '::' . $method, + (($value[0] === null) ? 'value' : 'instance/object' ) + )); + } else { + return false; + } + } else { + $resolvedParams[$index] = $value[3]; + } + + $index++; + } + + return $resolvedParams; // return ordered list of parameters + } + + /** + * Checks if the object has this class as one of its parents + * + * @see https://bugs.php.net/bug.php?id=53727 + * @see https://github.com/zendframework/zf2/pull/1807 + * + * @param string $className + * @param $type + * @return bool + */ + protected static function isSubclassOf($className, $type) + { + if (is_subclass_of($className, $type)) { + return true; + } + if (PHP_VERSION_ID >= 50307) { + return false; + } + if (!interface_exists($type)) { + return false; + } + $r = new ReflectionClass($className); + + return $r->implementsInterface($type); + } +} diff --git a/library/Zend/Di/Display/Console.php b/library/Zend/Di/Display/Console.php new file mode 100755 index 0000000000..31b861e638 --- /dev/null +++ b/library/Zend/Di/Display/Console.php @@ -0,0 +1,176 @@ +addRuntimeClasses($runtimeClasses); + $console->render($di); + } + + /** + * Constructor + * + * @param null|Di $di + */ + public function __construct(Di $di = null) + { + $this->di = ($di) ?: new Di; + } + + /** + * @param string[] $runtimeClasses + */ + public function addRuntimeClasses(array $runtimeClasses) + { + foreach ($runtimeClasses as $runtimeClass) { + $this->addRuntimeClass($runtimeClass); + } + } + + /** + * @param string $runtimeClass + */ + public function addRuntimeClass($runtimeClass) + { + $this->runtimeClasses[] = $runtimeClass; + } + + public function render() + { + $knownClasses = array(); + + echo 'Definitions' . PHP_EOL . PHP_EOL; + + foreach ($this->di->definitions() as $definition) { + $this->renderDefinition($definition); + foreach ($definition->getClasses() as $class) { + $knownClasses[] = $class; + $this->renderClassDefinition($definition, $class); + } + if (count($definition->getClasses()) == 0) { + echo PHP_EOL .' No Classes Found' . PHP_EOL . PHP_EOL; + } + } + + if ($this->runtimeClasses) { + echo ' Runtime classes:' . PHP_EOL; + } + + $unknownRuntimeClasses = array_diff($this->runtimeClasses, $knownClasses); + foreach ($unknownRuntimeClasses as $runtimeClass) { + $definition = $this->di->definitions()->getDefinitionForClass($runtimeClass); + $this->renderClassDefinition($definition, $runtimeClass); + } + + echo PHP_EOL . 'Instance Configuration Info:' . PHP_EOL; + + echo PHP_EOL . ' Aliases:' . PHP_EOL; + + $configuredTypes = array(); + foreach ($this->di->instanceManager()->getAliases() as $alias => $class) { + echo ' ' . $alias . ' [type: ' . $class . ']' . PHP_EOL; + $configuredTypes[] = $alias; + } + + echo PHP_EOL . ' Classes:' . PHP_EOL; + + foreach ($this->di->instanceManager()->getClasses() as $class) { + echo ' ' . $class . PHP_EOL; + $configuredTypes[] = $class; + } + + echo PHP_EOL . ' Configurations:' . PHP_EOL; + + foreach ($configuredTypes as $type) { + $info = $this->di->instanceManager()->getConfig($type); + echo ' ' . $type . PHP_EOL; + + if ($info['parameters']) { + echo ' parameters:' . PHP_EOL; + foreach ($info['parameters'] as $param => $value) { + echo ' ' . $param . ' = ' . $value . PHP_EOL; + } + } + + if ($info['injections']) { + echo ' injections:' . PHP_EOL; + foreach ($info['injections'] as $injection => $value) { + var_dump($injection, $value); + } + } + } + } + + /** + * @param object $definition + */ + protected function renderDefinition($definition) + { + echo ' Definition Type: ' . get_class($definition) . PHP_EOL; + $r = new \ReflectionClass($definition); + foreach ($r->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED) as $property) { + $property->setAccessible(true); + echo ' internal property: ' . $property->getName(); + $value = $property->getValue($definition); + if (is_object($value)) { + echo ' instance of ' . get_class($value); + } else { + echo ' = ' . $value; + } + echo PHP_EOL; + } + } + + /** + * @param \Zend\Di\Definition\DefinitionInterface $definition + * @param string $class + */ + protected function renderClassDefinition($definition, $class) + { + echo PHP_EOL . ' Parameters For Class: ' . $class . PHP_EOL; + foreach ($definition->getMethods($class) as $methodName => $methodIsRequired) { + foreach ($definition->getMethodParameters($class, $methodName) as $fqName => $pData) { + echo ' ' . $pData[0] . ' [type: '; + echo ($pData[1]) ? $pData[1] : 'scalar'; + echo ($pData[2] === true && $methodIsRequired) ? ', required' : ', not required'; + echo ', injection-method: ' . $methodName; + echo ' fq-name: ' . $fqName; + echo ']' . PHP_EOL; + } + } + echo PHP_EOL; + } +} diff --git a/library/Zend/Di/Exception/CircularDependencyException.php b/library/Zend/Di/Exception/CircularDependencyException.php new file mode 100755 index 0000000000..8cb9eae3f6 --- /dev/null +++ b/library/Zend/Di/Exception/CircularDependencyException.php @@ -0,0 +1,16 @@ + array(), 'hashLong' => array()); + + /** + * Array of class aliases + * @var array key: alias, value: class + */ + protected $aliases = array(); + + /** + * The template to use for housing configuration information + * @var array + */ + protected $configurationTemplate = array( + /** + * alias|class => alias|class + * interface|abstract => alias|class|object + * name => value + */ + 'parameters' => array(), + /** + * injection type => array of ordered method params + */ + 'injections' => array(), + /** + * alias|class => bool + */ + 'shared' => true + ); + + /** + * An array of instance configuration data + * @var array + */ + protected $configurations = array(); + + /** + * An array of globally preferred implementations for interfaces/abstracts + * @var array + */ + protected $typePreferences = array(); + + /** + * Does this instance manager have this shared instance + * @param string $classOrAlias + * @return bool + */ + public function hasSharedInstance($classOrAlias) + { + return isset($this->sharedInstances[$classOrAlias]); + } + + /** + * getSharedInstance() + */ + public function getSharedInstance($classOrAlias) + { + return $this->sharedInstances[$classOrAlias]; + } + + /** + * Add shared instance + * + * @param object $instance + * @param string $classOrAlias + * @throws Exception\InvalidArgumentException + */ + public function addSharedInstance($instance, $classOrAlias) + { + if (!is_object($instance)) { + throw new Exception\InvalidArgumentException('This method requires an object to be shared. Class or Alias given: ' . $classOrAlias); + } + + $this->sharedInstances[$classOrAlias] = $instance; + } + + /** + * hasSharedInstanceWithParameters() + * + * @param string $classOrAlias + * @param array $params + * @param bool $returnFastHashLookupKey + * @return bool|string + */ + public function hasSharedInstanceWithParameters($classOrAlias, array $params, $returnFastHashLookupKey = false) + { + ksort($params); + $hashKey = $this->createHashForKeys($classOrAlias, array_keys($params)); + if (isset($this->sharedInstancesWithParams['hashShort'][$hashKey])) { + $hashValue = $this->createHashForValues($classOrAlias, $params); + if (isset($this->sharedInstancesWithParams['hashLong'][$hashKey . '/' . $hashValue])) { + return ($returnFastHashLookupKey) ? $hashKey . '/' . $hashValue : true; + } + } + + return false; + } + + /** + * addSharedInstanceWithParameters() + * + * @param object $instance + * @param string $classOrAlias + * @param array $params + * @return void + */ + public function addSharedInstanceWithParameters($instance, $classOrAlias, array $params) + { + ksort($params); + $hashKey = $this->createHashForKeys($classOrAlias, array_keys($params)); + $hashValue = $this->createHashForValues($classOrAlias, $params); + + if (!isset($this->sharedInstancesWithParams[$hashKey]) + || !is_array($this->sharedInstancesWithParams[$hashKey])) { + $this->sharedInstancesWithParams[$hashKey] = array(); + } + + $this->sharedInstancesWithParams['hashShort'][$hashKey] = true; + $this->sharedInstancesWithParams['hashLong'][$hashKey . '/' . $hashValue] = $instance; + } + + /** + * Retrieves an instance by its name and the parameters stored at its instantiation + * + * @param string $classOrAlias + * @param array $params + * @param bool|null $fastHashFromHasLookup + * @return object|bool false if no instance was found + */ + public function getSharedInstanceWithParameters($classOrAlias, array $params, $fastHashFromHasLookup = null) + { + if ($fastHashFromHasLookup) { + return $this->sharedInstancesWithParams['hashLong'][$fastHashFromHasLookup]; + } + + ksort($params); + $hashKey = $this->createHashForKeys($classOrAlias, array_keys($params)); + if (isset($this->sharedInstancesWithParams['hashShort'][$hashKey])) { + $hashValue = $this->createHashForValues($classOrAlias, $params); + if (isset($this->sharedInstancesWithParams['hashLong'][$hashKey . '/' . $hashValue])) { + return $this->sharedInstancesWithParams['hashLong'][$hashKey . '/' . $hashValue]; + } + } + + return false; + } + + /** + * Check for an alias + * + * @param string $alias + * @return bool + */ + public function hasAlias($alias) + { + return (isset($this->aliases[$alias])); + } + + /** + * Get aliases + * + * @return array + */ + public function getAliases() + { + return $this->aliases; + } + + /** + * getClassFromAlias() + * + * @param string + * @return string|bool + * @throws Exception\RuntimeException + */ + public function getClassFromAlias($alias) + { + if (!isset($this->aliases[$alias])) { + return false; + } + $r = 0; + while (isset($this->aliases[$alias])) { + $alias = $this->aliases[$alias]; + $r++; + if ($r > 100) { + throw new Exception\RuntimeException( + sprintf('Possible infinite recursion in DI alias! Max recursion of 100 levels reached at alias "%s".', $alias) + ); + } + } + + return $alias; + } + + /** + * @param string $alias + * @return string|bool + * @throws Exception\RuntimeException + */ + protected function getBaseAlias($alias) + { + if (!$this->hasAlias($alias)) { + return false; + } + $lastAlias = false; + $r = 0; + while (isset($this->aliases[$alias])) { + $lastAlias = $alias; + $alias = $this->aliases[$alias]; + $r++; + if ($r > 100) { + throw new Exception\RuntimeException( + sprintf('Possible infinite recursion in DI alias! Max recursion of 100 levels reached at alias "%s".', $alias) + ); + } + } + + return $lastAlias; + } + + /** + * Add alias + * + * @throws Exception\InvalidArgumentException + * @param string $alias + * @param string $class + * @param array $parameters + * @return void + */ + public function addAlias($alias, $class, array $parameters = array()) + { + if (!preg_match('#^[a-zA-Z0-9-_]+$#', $alias)) { + throw new Exception\InvalidArgumentException( + 'Aliases must be alphanumeric and can contain dashes and underscores only.' + ); + } + $this->aliases[$alias] = $class; + if ($parameters) { + $this->setParameters($alias, $parameters); + } + } + + /** + * Check for configuration + * + * @param string $aliasOrClass + * @return bool + */ + public function hasConfig($aliasOrClass) + { + $key = ($this->hasAlias($aliasOrClass)) ? 'alias:' . $this->getBaseAlias($aliasOrClass) : $aliasOrClass; + if (!isset($this->configurations[$key])) { + return false; + } + if ($this->configurations[$key] === $this->configurationTemplate) { + return false; + } + + return true; + } + + /** + * Sets configuration for a single alias/class + * + * @param string $aliasOrClass + * @param array $configuration + * @param bool $append + */ + public function setConfig($aliasOrClass, array $configuration, $append = false) + { + $key = ($this->hasAlias($aliasOrClass)) ? 'alias:' . $this->getBaseAlias($aliasOrClass) : $aliasOrClass; + if (!isset($this->configurations[$key]) || !$append) { + $this->configurations[$key] = $this->configurationTemplate; + } + // Ignore anything but 'parameters' and 'injections' + $configuration = array( + 'parameters' => isset($configuration['parameters']) ? $configuration['parameters'] : array(), + 'injections' => isset($configuration['injections']) ? $configuration['injections'] : array(), + 'shared' => isset($configuration['shared']) ? $configuration['shared'] : true + ); + $this->configurations[$key] = array_replace_recursive($this->configurations[$key], $configuration); + } + + /** + * Get classes + * + * @return array + */ + public function getClasses() + { + $classes = array(); + foreach ($this->configurations as $name => $data) { + if (strpos($name, 'alias') === 0) { + continue; + } + $classes[] = $name; + } + + return $classes; + } + + /** + * @param string $aliasOrClass + * @return array + */ + public function getConfig($aliasOrClass) + { + $key = ($this->hasAlias($aliasOrClass)) ? 'alias:' . $this->getBaseAlias($aliasOrClass) : $aliasOrClass; + if (isset($this->configurations[$key])) { + return $this->configurations[$key]; + } + + return $this->configurationTemplate; + } + + /** + * setParameters() is a convenience method for: + * setConfig($type, array('parameters' => array(...)), true); + * + * @param string $aliasOrClass Alias or Class + * @param array $parameters Multi-dim array of parameters and their values + * @return void + */ + public function setParameters($aliasOrClass, array $parameters) + { + $this->setConfig($aliasOrClass, array('parameters' => $parameters), true); + } + + /** + * setInjections() is a convenience method for: + * setConfig($type, array('injections' => array(...)), true); + * + * @param string $aliasOrClass Alias or Class + * @param array $injections Multi-dim array of methods and their parameters + * @return void + */ + public function setInjections($aliasOrClass, array $injections) + { + $this->setConfig($aliasOrClass, array('injections' => $injections), true); + } + + /** + * Set shared + * + * @param string $aliasOrClass + * @param bool $isShared + * @return void + */ + public function setShared($aliasOrClass, $isShared) + { + $this->setConfig($aliasOrClass, array('shared' => (bool) $isShared), true); + } + + /** + * Check for type preferences + * + * @param string $interfaceOrAbstract + * @return bool + */ + public function hasTypePreferences($interfaceOrAbstract) + { + $key = ($this->hasAlias($interfaceOrAbstract)) ? 'alias:' . $interfaceOrAbstract : $interfaceOrAbstract; + + return (isset($this->typePreferences[$key]) && $this->typePreferences[$key]); + } + + /** + * Set type preference + * + * @param string $interfaceOrAbstract + * @param array $preferredImplementations + * @return InstanceManager + */ + public function setTypePreference($interfaceOrAbstract, array $preferredImplementations) + { + $key = ($this->hasAlias($interfaceOrAbstract)) ? 'alias:' . $interfaceOrAbstract : $interfaceOrAbstract; + foreach ($preferredImplementations as $preferredImplementation) { + $this->addTypePreference($key, $preferredImplementation); + } + + return $this; + } + + /** + * Get type preferences + * + * @param string $interfaceOrAbstract + * @return array + */ + public function getTypePreferences($interfaceOrAbstract) + { + $key = ($this->hasAlias($interfaceOrAbstract)) ? 'alias:' . $interfaceOrAbstract : $interfaceOrAbstract; + if (isset($this->typePreferences[$key])) { + return $this->typePreferences[$key]; + } + + return array(); + } + + /** + * Unset type preferences + * + * @param string $interfaceOrAbstract + * @return void + */ + public function unsetTypePreferences($interfaceOrAbstract) + { + $key = ($this->hasAlias($interfaceOrAbstract)) ? 'alias:' . $interfaceOrAbstract : $interfaceOrAbstract; + unset($this->typePreferences[$key]); + } + + /** + * Adds a type preference. A type preference is a redirection to a preferred alias or type when an abstract type + * $interfaceOrAbstract is requested + * + * @param string $interfaceOrAbstract + * @param string $preferredImplementation + * @return self + */ + public function addTypePreference($interfaceOrAbstract, $preferredImplementation) + { + $key = ($this->hasAlias($interfaceOrAbstract)) ? 'alias:' . $interfaceOrAbstract : $interfaceOrAbstract; + if (!isset($this->typePreferences[$key])) { + $this->typePreferences[$key] = array(); + } + $this->typePreferences[$key][] = $preferredImplementation; + + return $this; + } + + /** + * Removes a previously set type preference + * + * @param string $interfaceOrAbstract + * @param string $preferredType + * @return bool|self + */ + public function removeTypePreference($interfaceOrAbstract, $preferredType) + { + $key = ($this->hasAlias($interfaceOrAbstract)) ? 'alias:' . $interfaceOrAbstract : $interfaceOrAbstract; + if (!isset($this->typePreferences[$key]) || !in_array($preferredType, $this->typePreferences[$key])) { + return false; + } + unset($this->typePreferences[$key][array_search($key, $this->typePreferences)]); + + return $this; + } + + /** + * @param string $classOrAlias + * @param string[] $paramKeys + * @return string + */ + protected function createHashForKeys($classOrAlias, $paramKeys) + { + return $classOrAlias . ':' . implode('|', $paramKeys); + } + + /** + * @param string $classOrAlias + * @param array $paramValues + * @return string + */ + protected function createHashForValues($classOrAlias, $paramValues) + { + $hashValue = ''; + foreach ($paramValues as $param) { + switch (gettype($param)) { + case 'object': + $hashValue .= spl_object_hash($param) . '|'; + break; + case 'integer': + case 'string': + case 'boolean': + case 'NULL': + case 'double': + $hashValue .= $param . '|'; + break; + case 'array': + $hashValue .= 'Array|'; + break; + case 'resource': + $hashValue .= 'resource|'; + break; + } + } + + return $hashValue; + } +} diff --git a/library/Zend/Di/LocatorInterface.php b/library/Zend/Di/LocatorInterface.php new file mode 100755 index 0000000000..88a12f6cca --- /dev/null +++ b/library/Zend/Di/LocatorInterface.php @@ -0,0 +1,22 @@ + + * protected $map = array('foo' => 'getFoo'); + * + * + * When encountered, the return value of that method will be used. + * + * Methods mapped in this way may expect a single, array argument, the + * $params passed to {@link get()}, if any. + * + * @var array + */ + protected $map = array(); + + /** + * Registered services and cached values + * + * @var array + */ + protected $services = array(); + + /** + * {@inheritDoc} + */ + public function set($name, $service) + { + $this->services[$name] = $service; + + return $this; + } + + /** + * Retrieve a registered service + * + * Tests first if a value is registered for the service, and, if so, + * returns it. + * + * If the value returned is a non-object callback or closure, the return + * value is retrieved, stored, and returned. Parameters passed to the method + * are passed to the callback, but only on the first retrieval. + * + * If the service requested matches a method in the method map, the return + * value of that method is returned. Parameters are passed to the matching + * method. + * + * @param string $name + * @param array $params + * @return mixed + */ + public function get($name, array $params = array()) + { + if (!isset($this->services[$name])) { + if (!isset($this->map[$name])) { + return null; + } + $method = $this->map[$name]; + + return $this->$method($params); + } + + $service = $this->services[$name]; + if ($service instanceof Closure + || (!is_object($service) && is_callable($service)) + ) { + $this->services[$name] = $service = call_user_func_array($service, $params); + } + + return $service; + } +} diff --git a/library/Zend/Di/ServiceLocator/DependencyInjectorProxy.php b/library/Zend/Di/ServiceLocator/DependencyInjectorProxy.php new file mode 100755 index 0000000000..be0c3cb1d5 --- /dev/null +++ b/library/Zend/Di/ServiceLocator/DependencyInjectorProxy.php @@ -0,0 +1,168 @@ +di = $di; + $this->definitions = $di->definitions(); + $this->instanceManager = $di->instanceManager(); + } + + /** + * {@inheritDoc} + * @return GeneratorInstance + */ + public function get($name, array $params = array()) + { + return parent::get($name, $params); + } + + /** + * {@inheritDoc} + * @return GeneratorInstance + */ + public function newInstance($name, array $params = array(), $isShared = true) + { + $instance = parent::newInstance($name, $params, $isShared); + + if ($instance instanceof GeneratorInstance) { + /* @var $instance GeneratorInstance */ + $instance->setShared($isShared); + + // When a callback is used, we don't know instance the class name. + // That's why we assume $name as the instance alias + if (null === $instance->getName()) { + $instance->setAlias($name); + } + } + + return $instance; + } + + /** + * {@inheritDoc} + * @return GeneratorInstance + */ + public function createInstanceViaConstructor($class, $params, $alias = null) + { + $callParameters = array(); + + if ($this->di->definitions->hasMethod($class, '__construct') + && (count($this->di->definitions->getMethodParameters($class, '__construct')) > 0) + ) { + $callParameters = $this->resolveMethodParameters($class, '__construct', $params, $alias, true, true); + $callParameters = $callParameters ?: array(); + } + + return new GeneratorInstance($class, $alias, '__construct', $callParameters); + } + + /** + * {@inheritDoc} + * @throws \Zend\Di\Exception\InvalidCallbackException + * @return GeneratorInstance + */ + public function createInstanceViaCallback($callback, $params, $alias) + { + if (is_string($callback)) { + $callback = explode('::', $callback); + } + + if (!is_callable($callback)) { + throw new Exception\InvalidCallbackException('An invalid constructor callback was provided'); + } + + if (!is_array($callback) || is_object($callback[0])) { + throw new Exception\InvalidCallbackException( + 'For purposes of service locator generation, constructor callbacks must refer to static methods only' + ); + } + + $class = $callback[0]; + $method = $callback[1]; + + $callParameters = array(); + if ($this->di->definitions->hasMethod($class, $method)) { + $callParameters = $this->resolveMethodParameters($class, $method, $params, $alias, true, true); + } + + $callParameters = $callParameters ?: array(); + + return new GeneratorInstance(null, $alias, $callback, $callParameters); + } + + /** + * {@inheritDoc} + */ + public function handleInjectionMethodForObject($class, $method, $params, $alias, $isRequired) + { + return array( + 'method' => $method, + 'params' => $this->resolveMethodParameters($class, $method, $params, $alias, $isRequired), + ); + } + + /** + * {@inheritDoc} + */ + protected function resolveAndCallInjectionMethodForInstance($instance, $method, $params, $alias, $methodIsRequired, $methodClass = null) + { + if (!$instance instanceof GeneratorInstance) { + return parent::resolveAndCallInjectionMethodForInstance($instance, $method, $params, $alias, $methodIsRequired, $methodClass); + } + + /* @var $instance GeneratorInstance */ + $methodClass = $instance->getClass(); + $callParameters = $this->resolveMethodParameters($methodClass, $method, $params, $alias, $methodIsRequired); + + if ($callParameters !== false) { + $instance->addMethod(array( + 'method' => $method, + 'params' => $callParameters, + )); + + return true; + } + + return false; + } + + /** + * {@inheritDoc} + */ + protected function getClass($instance) + { + if ($instance instanceof GeneratorInstance) { + /* @var $instance GeneratorInstance */ + + return $instance->getClass(); + } + + return parent::getClass($instance); + } +} diff --git a/library/Zend/Di/ServiceLocator/Generator.php b/library/Zend/Di/ServiceLocator/Generator.php new file mode 100755 index 0000000000..82e8ca191e --- /dev/null +++ b/library/Zend/Di/ServiceLocator/Generator.php @@ -0,0 +1,342 @@ +injector = new DependencyInjectorProxy($injector); + } + + /** + * Set the class name for the generated service locator container + * + * @param string $name + * @return Generator + */ + public function setContainerClass($name) + { + $this->containerClass = $name; + + return $this; + } + + /** + * Set the namespace to use for the generated class file + * + * @param string $namespace + * @return Generator + */ + public function setNamespace($namespace) + { + $this->namespace = $namespace; + + return $this; + } + + /** + * Construct, configure, and return a PHP class file code generation object + * + * Creates a Zend\Code\Generator\FileGenerator object that has + * created the specified class and service locator methods. + * + * @param null|string $filename + * @throws \Zend\Di\Exception\RuntimeException + * @return FileGenerator + */ + public function getCodeGenerator($filename = null) + { + $injector = $this->injector; + $im = $injector->instanceManager(); + $indent = ' '; + $aliases = $this->reduceAliases($im->getAliases()); + $caseStatements = array(); + $getters = array(); + $definitions = $injector->definitions(); + + $fetched = array_unique(array_merge($definitions->getClasses(), $im->getAliases())); + + foreach ($fetched as $name) { + $getter = $this->normalizeAlias($name); + $meta = $injector->get($name); + $params = $meta->getParams(); + + // Build parameter list for instantiation + foreach ($params as $key => $param) { + if (null === $param || is_scalar($param) || is_array($param)) { + $string = var_export($param, 1); + if (strstr($string, '::__set_state(')) { + throw new Exception\RuntimeException('Arguments in definitions may not contain objects'); + } + $params[$key] = $string; + } elseif ($param instanceof GeneratorInstance) { + /* @var $param GeneratorInstance */ + $params[$key] = sprintf('$this->%s()', $this->normalizeAlias($param->getName())); + } else { + $message = sprintf('Unable to use object arguments when building containers. Encountered with "%s", parameter of type "%s"', $name, get_class($param)); + throw new Exception\RuntimeException($message); + } + } + + // Strip null arguments from the end of the params list + $reverseParams = array_reverse($params, true); + foreach ($reverseParams as $key => $param) { + if ('NULL' === $param) { + unset($params[$key]); + continue; + } + break; + } + + // Create instantiation code + $constructor = $meta->getConstructor(); + if ('__construct' != $constructor) { + // Constructor callback + $callback = var_export($constructor, 1); + if (strstr($callback, '::__set_state(')) { + throw new Exception\RuntimeException('Unable to build containers that use callbacks requiring object instances'); + } + if (count($params)) { + $creation = sprintf('$object = call_user_func(%s, %s);', $callback, implode(', ', $params)); + } else { + $creation = sprintf('$object = call_user_func(%s);', $callback); + } + } else { + // Normal instantiation + $className = '\\' . ltrim($name, '\\'); + $creation = sprintf('$object = new %s(%s);', $className, implode(', ', $params)); + } + + // Create method call code + $methods = ''; + foreach ($meta->getMethods() as $methodData) { + if (!isset($methodData['name']) && !isset($methodData['method'])) { + continue; + } + $methodName = isset($methodData['name']) ? $methodData['name'] : $methodData['method']; + $methodParams = $methodData['params']; + + // Create method parameter representation + foreach ($methodParams as $key => $param) { + if (null === $param || is_scalar($param) || is_array($param)) { + $string = var_export($param, 1); + if (strstr($string, '::__set_state(')) { + throw new Exception\RuntimeException('Arguments in definitions may not contain objects'); + } + $methodParams[$key] = $string; + } elseif ($param instanceof GeneratorInstance) { + $methodParams[$key] = sprintf('$this->%s()', $this->normalizeAlias($param->getName())); + } else { + $message = sprintf('Unable to use object arguments when generating method calls. Encountered with class "%s", method "%s", parameter of type "%s"', $name, $methodName, get_class($param)); + throw new Exception\RuntimeException($message); + } + } + + // Strip null arguments from the end of the params list + $reverseParams = array_reverse($methodParams, true); + foreach ($reverseParams as $key => $param) { + if ('NULL' === $param) { + unset($methodParams[$key]); + continue; + } + break; + } + + $methods .= sprintf("\$object->%s(%s);\n", $methodName, implode(', ', $methodParams)); + } + + // Generate caching statement + $storage = ''; + if ($im->hasSharedInstance($name, $params)) { + $storage = sprintf("\$this->services['%s'] = \$object;\n", $name); + } + + // Start creating getter + $getterBody = ''; + + // Create fetch of stored service + if ($im->hasSharedInstance($name, $params)) { + $getterBody .= sprintf("if (isset(\$this->services['%s'])) {\n", $name); + $getterBody .= sprintf("%sreturn \$this->services['%s'];\n}\n\n", $indent, $name); + } + + // Creation and method calls + $getterBody .= sprintf("%s\n", $creation); + $getterBody .= $methods; + + // Stored service + $getterBody .= $storage; + + // End getter body + $getterBody .= "return \$object;\n"; + + $getterDef = new MethodGenerator(); + $getterDef->setName($getter); + $getterDef->setBody($getterBody); + $getters[] = $getterDef; + + // Get cases for case statements + $cases = array($name); + if (isset($aliases[$name])) { + $cases = array_merge($aliases[$name], $cases); + } + + // Build case statement and store + $statement = ''; + foreach ($cases as $value) { + $statement .= sprintf("%scase '%s':\n", $indent, $value); + } + $statement .= sprintf("%sreturn \$this->%s();\n", str_repeat($indent, 2), $getter); + + $caseStatements[] = $statement; + } + + // Build switch statement + $switch = sprintf("switch (%s) {\n%s\n", '$name', implode("\n", $caseStatements)); + $switch .= sprintf("%sdefault:\n%sreturn parent::get(%s, %s);\n", $indent, str_repeat($indent, 2), '$name', '$params'); + $switch .= "}\n\n"; + + // Build get() method + $nameParam = new ParameterGenerator(); + $nameParam->setName('name'); + $paramsParam = new ParameterGenerator(); + $paramsParam->setName('params') + ->setType('array') + ->setDefaultValue(array()); + + $get = new MethodGenerator(); + $get->setName('get'); + $get->setParameters(array( + $nameParam, + $paramsParam, + )); + $get->setBody($switch); + + // Create getters for aliases + $aliasMethods = array(); + foreach ($aliases as $class => $classAliases) { + foreach ($classAliases as $alias) { + $aliasMethods[] = $this->getCodeGenMethodFromAlias($alias, $class); + } + } + + // Create class code generation object + $container = new ClassGenerator(); + $container->setName($this->containerClass) + ->setExtendedClass('ServiceLocator') + ->addMethodFromGenerator($get) + ->addMethods($getters) + ->addMethods($aliasMethods); + + // Create PHP file code generation object + $classFile = new FileGenerator(); + $classFile->setUse('Zend\Di\ServiceLocator') + ->setClass($container); + + if (null !== $this->namespace) { + $classFile->setNamespace($this->namespace); + } + + if (null !== $filename) { + $classFile->setFilename($filename); + } + + return $classFile; + } + + /** + * Reduces aliases + * + * Takes alias list and reduces it to a 2-dimensional array of + * class names pointing to an array of aliases that resolve to + * it. + * + * @param array $aliasList + * @return array + */ + protected function reduceAliases(array $aliasList) + { + $reduced = array(); + $aliases = array_keys($aliasList); + foreach ($aliasList as $alias => $service) { + if (in_array($service, $aliases)) { + do { + $service = $aliasList[$service]; + } while (in_array($service, $aliases)); + } + if (!isset($reduced[$service])) { + $reduced[$service] = array(); + } + $reduced[$service][] = $alias; + } + + return $reduced; + } + + /** + * Create a PhpMethod code generation object named after a given alias + * + * @param string $alias + * @param string $class Class to which alias refers + * @return MethodGenerator + */ + protected function getCodeGenMethodFromAlias($alias, $class) + { + $alias = $this->normalizeAlias($alias); + $method = new MethodGenerator(); + $method->setName($alias); + $method->setBody(sprintf('return $this->get(\'%s\');', $class)); + + return $method; + } + + /** + * Normalize an alias to a getter method name + * + * @param string $alias + * @return string + */ + protected function normalizeAlias($alias) + { + $normalized = preg_replace('/[^a-zA-Z0-9]/', ' ', $alias); + $normalized = 'get' . str_replace(' ', '', ucwords($normalized)); + + return $normalized; + } +} diff --git a/library/Zend/Di/ServiceLocator/GeneratorInstance.php b/library/Zend/Di/ServiceLocator/GeneratorInstance.php new file mode 100755 index 0000000000..aeb5f93acb --- /dev/null +++ b/library/Zend/Di/ServiceLocator/GeneratorInstance.php @@ -0,0 +1,196 @@ +class = $class; + $this->alias = $alias; + $this->constructor = $constructor; + $this->params = $params; + } + + /** + * Retrieves the best available name for this instance (instance alias first then class name) + * + * @return string|null + */ + public function getName() + { + return $this->alias ? $this->alias : $this->class; + } + + /** + * Class of the instance. Null if class is unclear (such as when the instance is produced by a callback) + * + * @return string|null + */ + public function getClass() + { + return $this->class; + } + + /** + * Alias for the instance (if any) + * + * @return string|null + */ + public function getAlias() + { + return $this->alias; + } + + /** + * Set class name + * + * In the case of an instance created via a callback, we need to set the + * class name after creating the generator instance. + * + * @param string $class + * @return GeneratorInstance + */ + public function setClass($class) + { + $this->class = $class; + + return $this; + } + + /** + * Set instance alias + * + * @param string $alias + * @return GeneratorInstance + */ + public function setAlias($alias) + { + $this->alias = $alias; + + return $this; + } + + /** + * Get instantiator + * + * @return mixed constructor method name or callable responsible for generating instance + */ + public function getConstructor() + { + return $this->constructor; + } + + /** + * Parameters passed to the instantiator as an ordered list of parameters. Each parameter that refers to another + * instance fetched recursively is a GeneratorInstance itself + * + * @return array + */ + public function getParams() + { + return $this->params; + } + + /** + * Set methods + * + * @param array $methods + * @return GeneratorInstance + */ + public function setMethods(array $methods) + { + $this->methods = $methods; + + return $this; + } + + /** + * Add a method called on the instance + * + * @param $method + * @return GeneratorInstance + */ + public function addMethod($method) + { + $this->methods[] = $method; + + return $this; + } + + /** + * Retrieves a list of methods that are called on the instance in their call order. Each returned element has form + * array('method' => 'methodName', 'params' => array( ... ordered list of call parameters ... ), where every call + * parameter that is a recursively fetched instance is a GeneratorInstance itself + * + * @return array + */ + public function getMethods() + { + return $this->methods; + } + + /** + * @param bool $shared + */ + public function setShared($shared) + { + $this->shared = (bool) $shared; + } + + /** + * Retrieves whether the instance is shared or not + * + * @return bool + */ + public function isShared() + { + return $this->shared; + } +} diff --git a/library/Zend/Di/ServiceLocatorInterface.php b/library/Zend/Di/ServiceLocatorInterface.php new file mode 100755 index 0000000000..fe5e125726 --- /dev/null +++ b/library/Zend/Di/ServiceLocatorInterface.php @@ -0,0 +1,23 @@ + array( + 'Zend\Foo\Bar' => array( + 'public' => true, + 'methods' => array( + '__construct' => array( + 'params' => array( + 'foo' => 'bar', + ), + 'class' => 'Some\Default\Class' + ), + 'setConfig' => array( + 'params' => array( + 'bar' => 'baz', + ), + ), + ), + ), + ), + ) + +- Ability to pass configuration to a generated ServiceLocator + +- Skip optional arguments if not passed in configuration or part of definition + (current behavior is to raise an exception if *any* arguments are missing) + +- Scoped Containers: + + Described here: + http://picocontainer.org/scopes.html + + This is something that should be explored when we start using these containers + with ServiceLocators inside an application. While part of this has to do with + garbage collection in Java (something we need not worry with in PHP since there + is no persistent in-memory objects), the interesting use case would be having a + container cloned from another container that has more or less (a subset) of the + definitions available to the Container's newInstance() and get() facilities. + +- Better Strategy Management + + Currently, the strategies for determining dependencies is hard coded into the + various definitions. Ideally, we'd be able to have configurable strategies + that the definitions can then utilize to do their job: + + http://picocontainer.org/injection.html + + We currently support constructor injection and setter injection (methods prefixed + by set[A-Z]) + +- Annotation Parsing + + Ideally, at some point, Zend\Code\Scanner will support Annotation parsing. When + this is possible, we'd like to be able to use @inject similar to + http://picocontainer.org/annotated-method-injection.html + +- SuperType Resolution + + (partially done inside resolveMethodParameters with is_subtype_of()) + + If a class claims it needs a dependency of not an object, but a particular + interface, the ability to find an object that suits that dependency, either + through a concept called 'Preferred Objects' or via a 'Property'. The + following should be supported: + + The compiler also needs to be aware of other definitions when looking up SuperTypes + + $definition = new AggregateDefinition(); + $definition->addDefinition('Zend\Controller\DiDefinition'); + + $compiler = new Compiler() + $compiler->addDefinition($definition); + $compiler->addCodeScanner(__DIR__ . 'My/Blog'); + $array = $compiler->compile() + $definition->addDefinition(new ArrayDefinition($array)); + +- Performance & Benchmarking + + Zend\Code\Scanner- check memory usage, perhaps use gc_collect_cycles() to free memory, + we'll have to do this on large scan bases. + + Benchmark compiler: reflection vs. code scanner + + diff --git a/library/Zend/Di/composer.json b/library/Zend/Di/composer.json new file mode 100755 index 0000000000..81f6cb7818 --- /dev/null +++ b/library/Zend/Di/composer.json @@ -0,0 +1,33 @@ +{ + "name": "zendframework/zend-di", + "description": " ", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "di" + ], + "homepage": "https://github.com/zendframework/zf2", + "autoload": { + "psr-0": { + "Zend\\Di\\": "" + } + }, + "target-dir": "Zend/Di", + "require": { + "php": ">=5.3.23", + "zendframework/zend-code": "self.version", + "zendframework/zend-stdlib": "self.version" + }, + "require-dev": { + "zendframework/zend-servicemanager": "self.version" + }, + "suggest": { + "zendframework/zend-servicemanager": "Zend\\ServiceManager component" + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev", + "dev-develop": "2.4-dev" + } + } +} diff --git a/library/Zend/Dom/CONTRIBUTING.md b/library/Zend/Dom/CONTRIBUTING.md new file mode 100755 index 0000000000..e77f5d2d5b --- /dev/null +++ b/library/Zend/Dom/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# CONTRIBUTING + +Please don't open pull requests against this repository, please use https://github.com/zendframework/zf2. \ No newline at end of file diff --git a/library/Zend/Dom/Css2Xpath.php b/library/Zend/Dom/Css2Xpath.php new file mode 100755 index 0000000000..7aa7d506cc --- /dev/null +++ b/library/Zend/Dom/Css2Xpath.php @@ -0,0 +1,33 @@ +errors = array(null); + + set_error_handler(array($this, 'addError'), \E_WARNING); + $nodeList = $this->query($expression); + restore_error_handler(); + + $exception = array_pop($this->errors); + if ($exception) { + throw $exception; + } + + return $nodeList; + } + + /** + * Adds an error to the stack of errors + * + * @param int $errno + * @param string $errstr + * @param string $errfile + * @param int $errline + * @return void + */ + public function addError($errno, $errstr = '', $errfile = '', $errline = 0) + { + $last_error = end($this->errors); + $this->errors[] = new ErrorException( + $errstr, + 0, + $errno, + $errfile, + $errline, + $last_error + ); + } +} diff --git a/library/Zend/Dom/Document.php b/library/Zend/Dom/Document.php new file mode 100755 index 0000000000..456e17f864 --- /dev/null +++ b/library/Zend/Dom/Document.php @@ -0,0 +1,310 @@ +setStringDocument($document, $type, $encoding); + } + + /** + * Get raw set document + * + * @return string|null + */ + public function getStringDocument() + { + return $this->stringDocument; + } + + /** + * Set raw document + * + * @param string|null $document + * @param string|null $forcedType Type for the provided document (see constants) + * @param string|null $forcedEncoding Encoding for the provided document + * @return self + */ + protected function setStringDocument($document, $forcedType = null, $forcedEncoding = null) + { + $type = static::DOC_HTML; + if (strstr($document, 'DTD XHTML')) { + $type = static::DOC_XHTML; + } + + // Breaking XML declaration to make syntax highlighting work + if ('<' . '?xml' == substr(trim($document), 0, 5)) { + $type = static::DOC_XML; + if (preg_match('/]*xmlns="([^"]+)"[^>]*>/i', $document, $matches)) { + $this->xpathNamespaces[] = $matches[1]; + $type = static::DOC_XHTML; + } + } + + // Unsetting previously registered DOMDocument + $this->domDocument = null; + $this->stringDocument = !empty($document) ? $document : null; + + $this->setType($forcedType ?: (!empty($document) ? $type : null)); + $this->setEncoding($forcedEncoding); + $this->setErrors(array()); + + return $this; + } + + /** + * Get raw document type + * + * @return string|null + */ + public function getType() + { + return $this->type; + } + + /** + * Set raw document type + * + * @param string $type + * @return self + */ + protected function setType($type) + { + $this->type = $type; + + return $this; + } + + /** + * Get DOMDocument generated from set raw document + * + * @return DOMDocument + * @throws Exception\RuntimeException If cannot get DOMDocument; no document registered + */ + public function getDomDocument() + { + if (null === ($stringDocument = $this->getStringDocument())) { + throw new Exception\RuntimeException('Cannot get DOMDocument; no document registered'); + } + + if (null === $this->domDocument) { + $this->domDocument = $this->getDomDocumentFromString($stringDocument); + } + + return $this->domDocument; + } + + /** + * Set DOMDocument + * + * @param DOMDocument $domDocument + * @return self + */ + protected function setDomDocument(DOMDocument $domDocument) + { + $this->domDocument = $domDocument; + + return $this; + } + + /** + * Get set document encoding + * + * @return string|null + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Set raw document encoding for DOMDocument generation + * + * @param string|null $encoding + * @return self + */ + public function setEncoding($encoding) + { + $this->encoding = $encoding; + + return $this->encoding; + } + + /** + * Get DOMDocument generation errors + * + * @return array + */ + public function getErrors() + { + return $this->errors; + } + + /** + * Set document errors from DOMDocument generation + * + * @param array $errors + * @return self + */ + protected function setErrors($errors) + { + $this->errors = $errors; + + return $this; + } + + /** + * Get DOMDocument from set raw document + * + * @return DOMDocument + * @throws Exception\RuntimeException + */ + protected function getDomDocumentFromString($stringDocument) + { + libxml_use_internal_errors(true); + libxml_disable_entity_loader(true); + + $encoding = $this->getEncoding(); + $domDoc = null === $encoding ? new DOMDocument('1.0') : new DOMDocument('1.0', $encoding); + $type = $this->getType(); + + switch ($type) { + case static::DOC_XML: + $success = $domDoc->loadXML($stringDocument); + foreach ($domDoc->childNodes as $child) { + if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) { + throw new Exception\RuntimeException( + 'Invalid XML: Detected use of illegal DOCTYPE' + ); + } + } + break; + case static::DOC_HTML: + case static::DOC_XHTML: + default: + $success = $domDoc->loadHTML($stringDocument); + break; + } + + $errors = libxml_get_errors(); + if (!empty($errors)) { + $this->setErrors($errors); + libxml_clear_errors(); + } + + libxml_disable_entity_loader(false); + libxml_use_internal_errors(false); + + if (!$success) { + throw new Exception\RuntimeException(sprintf('Error parsing document (type == %s)', $type)); + } + + return $domDoc; + } + + /** + * Get Document's registered XPath namespaces + * + * @return array + */ + public function getXpathNamespaces() + { + return $this->xpathNamespaces; + } + + /** + * Register XPath namespaces + * + * @param array $xpathNamespaces + * @return void + */ + public function registerXpathNamespaces($xpathNamespaces) + { + $this->xpathNamespaces = $xpathNamespaces; + } + + /** + * Get Document's registered XPath PHP Functions + * + * @return string|null + */ + public function getXpathPhpFunctions() + { + return $this->xpathPhpFunctions; + } + /** + * Register PHP Functions to use in internal DOMXPath + * + * @param bool $xpathPhpFunctions + * @return void + */ + public function registerXpathPhpFunctions($xpathPhpFunctions = true) + { + $this->xpathPhpFunctions = $xpathPhpFunctions; + } +} diff --git a/library/Zend/Dom/Document/NodeList.php b/library/Zend/Dom/Document/NodeList.php new file mode 100755 index 0000000000..352326fe27 --- /dev/null +++ b/library/Zend/Dom/Document/NodeList.php @@ -0,0 +1,160 @@ +list = $list; + } + + /** + * Iterator: rewind to first element + * + * @return DOMNode + */ + public function rewind() + { + $this->position = 0; + + return $this->list->item(0); + } + + /** + * Iterator: is current position valid? + * + * @return bool + */ + public function valid() + { + if (in_array($this->position, range(0, $this->list->length - 1)) && $this->list->length > 0) { + return true; + } + + return false; + } + + /** + * Iterator: return current element + * + * @return DOMNode + */ + public function current() + { + return $this->list->item($this->position); + } + + /** + * Iterator: return key of current element + * + * @return int + */ + public function key() + { + return $this->position; + } + + /** + * Iterator: move to next element + * + * @return DOMNode + */ + public function next() + { + ++$this->position; + + return $this->list->item($this->position); + } + + /** + * Countable: get count + * + * @return int + */ + public function count() + { + return $this->list->length; + } + + /** + * ArrayAccess: offset exists + * + * @param int $key + * @return bool + */ + public function offsetExists($key) + { + if (in_array($key, range(0, $this->list->length - 1)) && $this->list->length > 0) { + return true; + } + return false; + } + + /** + * ArrayAccess: get offset + * + * @param int $key + * @return mixed + */ + public function offsetGet($key) + { + return $this->list->item($key); + } + + /** + * ArrayAccess: set offset + * + * @param mixed $key + * @param mixed $value + * @throws Exception\BadMethodCallException when attempting to write to a read-only item + */ + public function offsetSet($key, $value) + { + throw new Exception\BadMethodCallException('Attempting to write to a read-only list'); + } + + /** + * ArrayAccess: unset offset + * + * @param mixed $key + * @throws Exception\BadMethodCallException when attempting to unset a read-only item + */ + public function offsetUnset($key) + { + throw new Exception\BadMethodCallException('Attempting to unset on a read-only list'); + } +} diff --git a/library/Zend/Dom/Document/Query.php b/library/Zend/Dom/Document/Query.php new file mode 100755 index 0000000000..bea590328a --- /dev/null +++ b/library/Zend/Dom/Document/Query.php @@ -0,0 +1,169 @@ +getDomDocument()); + + $xpathNamespaces = $document->getXpathNamespaces(); + foreach ($xpathNamespaces as $prefix => $namespaceUri) { + $xpath->registerNamespace($prefix, $namespaceUri); + } + + if ($xpathPhpfunctions = $document->getXpathPhpFunctions()) { + $xpath->registerNamespace('php', 'http://php.net/xpath'); + ($xpathPhpfunctions === true) ? $xpath->registerPHPFunctions() : $xpath->registerPHPFunctions($xpathPhpfunctions); + } + + $nodeList = $xpath->queryWithErrorException($expression); + return new NodeList($nodeList); + } + + /** + * Transform CSS expression to XPath + * + * @param string $path + * @return string + */ + public static function cssToXpath($path) + { + $path = (string) $path; + if (strstr($path, ',')) { + $paths = explode(',', $path); + $expressions = array(); + foreach ($paths as $path) { + $xpath = static::cssToXpath(trim($path)); + if (is_string($xpath)) { + $expressions[] = $xpath; + } elseif (is_array($xpath)) { + $expressions = array_merge($expressions, $xpath); + } + } + return implode('|', $expressions); + } + + $paths = array('//'); + $path = preg_replace('|\s+>\s+|', '>', $path); + $segments = preg_split('/\s+/', $path); + foreach ($segments as $key => $segment) { + $pathSegment = static::_tokenize($segment); + if (0 == $key) { + if (0 === strpos($pathSegment, '[contains(')) { + $paths[0] .= '*' . ltrim($pathSegment, '*'); + } else { + $paths[0] .= $pathSegment; + } + continue; + } + if (0 === strpos($pathSegment, '[contains(')) { + foreach ($paths as $pathKey => $xpath) { + $paths[$pathKey] .= '//*' . ltrim($pathSegment, '*'); + $paths[] = $xpath . $pathSegment; + } + } else { + foreach ($paths as $pathKey => $xpath) { + $paths[$pathKey] .= '//' . $pathSegment; + } + } + } + + if (1 == count($paths)) { + return $paths[0]; + } + return implode('|', $paths); + } + + /** + * Tokenize CSS expressions to XPath + * + * @param string $expression + * @return string + */ + protected static function _tokenize($expression) + { + // Child selectors + $expression = str_replace('>', '/', $expression); + + // IDs + $expression = preg_replace('|#([a-z][a-z0-9_-]*)|i', '[@id=\'$1\']', $expression); + $expression = preg_replace('|(?cssQuery = $cssQuery; + $this->xpathQuery = $xpathQuery; + $this->document = $document; + $this->nodeList = $nodeList; + } + + /** + * Retrieve CSS Query + * + * @return string + */ + public function getCssQuery() + { + return $this->cssQuery; + } + + /** + * Retrieve XPath query + * + * @return string + */ + public function getXpathQuery() + { + return $this->xpathQuery; + } + + /** + * Retrieve DOMDocument + * + * @return DOMDocument + */ + public function getDocument() + { + return $this->document; + } + + /** + * Iterator: rewind to first element + * + * @return DOMNode + */ + public function rewind() + { + $this->position = 0; + + return $this->nodeList->item(0); + } + + /** + * Iterator: is current position valid? + * + * @return bool + */ + public function valid() + { + if (in_array($this->position, range(0, $this->nodeList->length - 1)) && $this->nodeList->length > 0) { + return true; + } + + return false; + } + + /** + * Iterator: return current element + * + * @return DOMNode + */ + public function current() + { + return $this->nodeList->item($this->position); + } + + /** + * Iterator: return key of current element + * + * @return int + */ + public function key() + { + return $this->position; + } + + /** + * Iterator: move to next element + * + * @return DOMNode + */ + public function next() + { + ++$this->position; + + return $this->nodeList->item($this->position); + } + + /** + * Countable: get count + * + * @return int + */ + public function count() + { + return $this->nodeList->length; + } + + /** + * ArrayAccess: offset exists + * + * @param int $key + * @return bool + */ + public function offsetExists($key) + { + if (in_array($key, range(0, $this->nodeList->length - 1)) && $this->nodeList->length > 0) { + return true; + } + return false; + } + + /** + * ArrayAccess: get offset + * + * @param int $key + * @return mixed + */ + public function offsetGet($key) + { + return $this->nodeList->item($key); + } + + /** + * ArrayAccess: set offset + * + * @param mixed $key + * @param mixed $value + * @throws Exception\BadMethodCallException when attempting to write to a read-only item + */ + public function offsetSet($key, $value) + { + throw new Exception\BadMethodCallException('Attempting to write to a read-only list'); + } + + /** + * ArrayAccess: unset offset + * + * @param mixed $key + * @throws Exception\BadMethodCallException when attempting to unset a read-only item + */ + public function offsetUnset($key) + { + throw new Exception\BadMethodCallException('Attempting to unset on a read-only list'); + } +} diff --git a/library/Zend/Dom/Query.php b/library/Zend/Dom/Query.php new file mode 100755 index 0000000000..5514299169 --- /dev/null +++ b/library/Zend/Dom/Query.php @@ -0,0 +1,320 @@ +setEncoding($encoding); + $this->setDocument($document); + } + + /** + * Set document encoding + * + * @param string $encoding + * @return Query + */ + public function setEncoding($encoding) + { + $this->encoding = (null === $encoding) ? null : (string) $encoding; + return $this; + } + + /** + * Get document encoding + * + * @return null|string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Set document to query + * + * @param string $document + * @param null|string $encoding Document encoding + * @return Query + */ + public function setDocument($document, $encoding = null) + { + if (0 === strlen($document)) { + return $this; + } + // breaking XML declaration to make syntax highlighting work + if ('<' . '?xml' == substr(trim($document), 0, 5)) { + if (preg_match('/]*xmlns="([^"]+)"[^>]*>/i', $document, $matches)) { + $this->xpathNamespaces[] = $matches[1]; + return $this->setDocumentXhtml($document, $encoding); + } + return $this->setDocumentXml($document, $encoding); + } + if (strstr($document, 'DTD XHTML')) { + return $this->setDocumentXhtml($document, $encoding); + } + return $this->setDocumentHtml($document, $encoding); + } + + /** + * Register HTML document + * + * @param string $document + * @param null|string $encoding Document encoding + * @return Query + */ + public function setDocumentHtml($document, $encoding = null) + { + $this->document = (string) $document; + $this->docType = self::DOC_HTML; + if (null !== $encoding) { + $this->setEncoding($encoding); + } + return $this; + } + + /** + * Register XHTML document + * + * @param string $document + * @param null|string $encoding Document encoding + * @return Query + */ + public function setDocumentXhtml($document, $encoding = null) + { + $this->document = (string) $document; + $this->docType = self::DOC_XHTML; + if (null !== $encoding) { + $this->setEncoding($encoding); + } + return $this; + } + + /** + * Register XML document + * + * @param string $document + * @param null|string $encoding Document encoding + * @return Query + */ + public function setDocumentXml($document, $encoding = null) + { + $this->document = (string) $document; + $this->docType = self::DOC_XML; + if (null !== $encoding) { + $this->setEncoding($encoding); + } + return $this; + } + + /** + * Retrieve current document + * + * @return string + */ + public function getDocument() + { + return $this->document; + } + + /** + * Get document type + * + * @return string + */ + public function getDocumentType() + { + return $this->docType; + } + + /** + * Get any DOMDocument errors found + * + * @return false|array + */ + public function getDocumentErrors() + { + return $this->documentErrors; + } + + /** + * Perform a CSS selector query + * + * @param string $query + * @return NodeList + */ + public function execute($query) + { + $xpathQuery = Document\Query::cssToXpath($query); + return $this->queryXpath($xpathQuery, $query); + } + + /** + * Perform an XPath query + * + * @param string|array $xpathQuery + * @param string|null $query CSS selector query + * @throws Exception\RuntimeException + * @return NodeList + */ + public function queryXpath($xpathQuery, $query = null) + { + if (null === ($document = $this->getDocument())) { + throw new Exception\RuntimeException('Cannot query; no document registered'); + } + + $encoding = $this->getEncoding(); + libxml_use_internal_errors(true); + libxml_disable_entity_loader(true); + if (null === $encoding) { + $domDoc = new DOMDocument('1.0'); + } else { + $domDoc = new DOMDocument('1.0', $encoding); + } + $type = $this->getDocumentType(); + switch ($type) { + case self::DOC_XML: + $success = $domDoc->loadXML($document); + foreach ($domDoc->childNodes as $child) { + if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) { + throw new Exception\RuntimeException( + 'Invalid XML: Detected use of illegal DOCTYPE' + ); + } + } + break; + case self::DOC_HTML: + case self::DOC_XHTML: + default: + $success = $domDoc->loadHTML($document); + break; + } + $errors = libxml_get_errors(); + if (!empty($errors)) { + $this->documentErrors = $errors; + libxml_clear_errors(); + } + libxml_disable_entity_loader(false); + libxml_use_internal_errors(false); + + if (!$success) { + throw new Exception\RuntimeException(sprintf('Error parsing document (type == %s)', $type)); + } + + $nodeList = $this->getNodeList($domDoc, $xpathQuery); + return new NodeList($query, $xpathQuery, $domDoc, $nodeList); + } + + /** + * Register XPath namespaces + * + * @param array $xpathNamespaces + * @return void + */ + public function registerXpathNamespaces($xpathNamespaces) + { + $this->xpathNamespaces = $xpathNamespaces; + } + + /** + * Register PHP Functions to use in internal DOMXPath + * + * @param bool $xpathPhpFunctions + * @return void + */ + public function registerXpathPhpFunctions($xpathPhpFunctions = true) + { + $this->xpathPhpFunctions = $xpathPhpFunctions; + } + + /** + * Prepare node list + * + * @param DOMDocument $document + * @param string|array $xpathQuery + * @return array + * @throws \ErrorException If query cannot be executed + */ + protected function getNodeList($document, $xpathQuery) + { + $xpath = new DOMXPath($document); + foreach ($this->xpathNamespaces as $prefix => $namespaceUri) { + $xpath->registerNamespace($prefix, $namespaceUri); + } + if ($this->xpathPhpFunctions) { + $xpath->registerNamespace("php", "http://php.net/xpath"); + ($this->xpathPhpFunctions === true) ? + $xpath->registerPHPFunctions() + : $xpath->registerPHPFunctions($this->xpathPhpFunctions); + } + $xpathQuery = (string) $xpathQuery; + + $nodeList = $xpath->queryWithErrorException($xpathQuery); + return $nodeList; + } +} diff --git a/library/Zend/Dom/README.md b/library/Zend/Dom/README.md new file mode 100755 index 0000000000..b82c2b9ffc --- /dev/null +++ b/library/Zend/Dom/README.md @@ -0,0 +1,15 @@ +DOM Component from ZF2 +====================== + +This is the DOM component for ZF2. + +- File issues at https://github.com/zendframework/zf2/issues +- Create pull requests against https://github.com/zendframework/zf2 +- Documentation is at http://framework.zend.com/docs + +LICENSE +------- + +The files in this archive are released under the [Zend Framework +license](http://framework.zend.com/license), which is a 3-clause BSD license. + diff --git a/library/Zend/Dom/composer.json b/library/Zend/Dom/composer.json new file mode 100755 index 0000000000..94433c52af --- /dev/null +++ b/library/Zend/Dom/composer.json @@ -0,0 +1,25 @@ +{ + "name": "zendframework/zend-dom", + "description": "provides tools for working with DOM documents and structures", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "dom" + ], + "homepage": "https://github.com/zendframework/zf2", + "autoload": { + "psr-0": { + "Zend\\Dom\\": "" + } + }, + "target-dir": "Zend/Dom", + "require": { + "php": ">=5.3.23" + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev", + "dev-develop": "2.4-dev" + } + } +} diff --git a/library/Zend/Escaper/CONTRIBUTING.md b/library/Zend/Escaper/CONTRIBUTING.md new file mode 100755 index 0000000000..e77f5d2d5b --- /dev/null +++ b/library/Zend/Escaper/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# CONTRIBUTING + +Please don't open pull requests against this repository, please use https://github.com/zendframework/zf2. \ No newline at end of file diff --git a/library/Zend/Escaper/Escaper.php b/library/Zend/Escaper/Escaper.php new file mode 100755 index 0000000000..3a3d30eb6c --- /dev/null +++ b/library/Zend/Escaper/Escaper.php @@ -0,0 +1,387 @@ + 'quot', // quotation mark + 38 => 'amp', // ampersand + 60 => 'lt', // less-than sign + 62 => 'gt', // greater-than sign + ); + + /** + * Current encoding for escaping. If not UTF-8, we convert strings from this encoding + * pre-escaping and back to this encoding post-escaping. + * + * @var string + */ + protected $encoding = 'utf-8'; + + /** + * Holds the value of the special flags passed as second parameter to + * htmlspecialchars(). We modify these for PHP 5.4 to take advantage + * of the new ENT_SUBSTITUTE flag for correctly dealing with invalid + * UTF-8 sequences. + * + * @var string + */ + protected $htmlSpecialCharsFlags = ENT_QUOTES; + + /** + * Static Matcher which escapes characters for HTML Attribute contexts + * + * @var callable + */ + protected $htmlAttrMatcher; + + /** + * Static Matcher which escapes characters for Javascript contexts + * + * @var callable + */ + protected $jsMatcher; + + /** + * Static Matcher which escapes characters for CSS Attribute contexts + * + * @var callable + */ + protected $cssMatcher; + + /** + * List of all encoding supported by this class + * + * @var array + */ + protected $supportedEncodings = array( + 'iso-8859-1', 'iso8859-1', 'iso-8859-5', 'iso8859-5', + 'iso-8859-15', 'iso8859-15', 'utf-8', 'cp866', + 'ibm866', '866', 'cp1251', 'windows-1251', + 'win-1251', '1251', 'cp1252', 'windows-1252', + '1252', 'koi8-r', 'koi8-ru', 'koi8r', + 'big5', '950', 'gb2312', '936', + 'big5-hkscs', 'shift_jis', 'sjis', 'sjis-win', + 'cp932', '932', 'euc-jp', 'eucjp', + 'eucjp-win', 'macroman' + ); + + /** + * Constructor: Single parameter allows setting of global encoding for use by + * the current object. If PHP 5.4 is detected, additional ENT_SUBSTITUTE flag + * is set for htmlspecialchars() calls. + * + * @param string $encoding + * @throws Exception\InvalidArgumentException + */ + public function __construct($encoding = null) + { + if ($encoding !== null) { + $encoding = (string) $encoding; + if ($encoding === '') { + throw new Exception\InvalidArgumentException( + get_class($this) . ' constructor parameter does not allow a blank value' + ); + } + + $encoding = strtolower($encoding); + if (!in_array($encoding, $this->supportedEncodings)) { + throw new Exception\InvalidArgumentException( + 'Value of \'' . $encoding . '\' passed to ' . get_class($this) + . ' constructor parameter is invalid. Provide an encoding supported by htmlspecialchars()' + ); + } + + $this->encoding = $encoding; + } + + if (defined('ENT_SUBSTITUTE')) { + $this->htmlSpecialCharsFlags|= ENT_SUBSTITUTE; + } + + // set matcher callbacks + $this->htmlAttrMatcher = array($this, 'htmlAttrMatcher'); + $this->jsMatcher = array($this, 'jsMatcher'); + $this->cssMatcher = array($this, 'cssMatcher'); + } + + /** + * Return the encoding that all output/input is expected to be encoded in. + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Escape a string for the HTML Body context where there are very few characters + * of special meaning. Internally this will use htmlspecialchars(). + * + * @param string $string + * @return string + */ + public function escapeHtml($string) + { + return htmlspecialchars($string, $this->htmlSpecialCharsFlags, $this->encoding); + } + + /** + * Escape a string for the HTML Attribute context. We use an extended set of characters + * to escape that are not covered by htmlspecialchars() to cover cases where an attribute + * might be unquoted or quoted illegally (e.g. backticks are valid quotes for IE). + * + * @param string $string + * @return string + */ + public function escapeHtmlAttr($string) + { + $string = $this->toUtf8($string); + if ($string === '' || ctype_digit($string)) { + return $string; + } + + $result = preg_replace_callback('/[^a-z0-9,\.\-_]/iSu', $this->htmlAttrMatcher, $string); + return $this->fromUtf8($result); + } + + /** + * Escape a string for the Javascript context. This does not use json_encode(). An extended + * set of characters are escaped beyond ECMAScript's rules for Javascript literal string + * escaping in order to prevent misinterpretation of Javascript as HTML leading to the + * injection of special characters and entities. The escaping used should be tolerant + * of cases where HTML escaping was not applied on top of Javascript escaping correctly. + * Backslash escaping is not used as it still leaves the escaped character as-is and so + * is not useful in a HTML context. + * + * @param string $string + * @return string + */ + public function escapeJs($string) + { + $string = $this->toUtf8($string); + if ($string === '' || ctype_digit($string)) { + return $string; + } + + $result = preg_replace_callback('/[^a-z0-9,\._]/iSu', $this->jsMatcher, $string); + return $this->fromUtf8($result); + } + + /** + * Escape a string for the URI or Parameter contexts. This should not be used to escape + * an entire URI - only a subcomponent being inserted. The function is a simple proxy + * to rawurlencode() which now implements RFC 3986 since PHP 5.3 completely. + * + * @param string $string + * @return string + */ + public function escapeUrl($string) + { + return rawurlencode($string); + } + + /** + * Escape a string for the CSS context. CSS escaping can be applied to any string being + * inserted into CSS and escapes everything except alphanumerics. + * + * @param string $string + * @return string + */ + public function escapeCss($string) + { + $string = $this->toUtf8($string); + if ($string === '' || ctype_digit($string)) { + return $string; + } + + $result = preg_replace_callback('/[^a-z0-9]/iSu', $this->cssMatcher, $string); + return $this->fromUtf8($result); + } + + /** + * Callback function for preg_replace_callback that applies HTML Attribute + * escaping to all matches. + * + * @param array $matches + * @return string + */ + protected function htmlAttrMatcher($matches) + { + $chr = $matches[0]; + $ord = ord($chr); + + /** + * The following replaces characters undefined in HTML with the + * hex entity for the Unicode replacement character. + */ + if (($ord <= 0x1f && $chr != "\t" && $chr != "\n" && $chr != "\r") + || ($ord >= 0x7f && $ord <= 0x9f) + ) { + return '�'; + } + + /** + * Check if the current character to escape has a name entity we should + * replace it with while grabbing the integer value of the character. + */ + if (strlen($chr) > 1) { + $chr = $this->convertEncoding($chr, 'UTF-16BE', 'UTF-8'); + } + + $hex = bin2hex($chr); + $ord = hexdec($hex); + if (isset(static::$htmlNamedEntityMap[$ord])) { + return '&' . static::$htmlNamedEntityMap[$ord] . ';'; + } + + /** + * Per OWASP recommendations, we'll use upper hex entities + * for any other characters where a named entity does not exist. + */ + if ($ord > 255) { + return sprintf('&#x%04X;', $ord); + } + return sprintf('&#x%02X;', $ord); + } + + /** + * Callback function for preg_replace_callback that applies Javascript + * escaping to all matches. + * + * @param array $matches + * @return string + */ + protected function jsMatcher($matches) + { + $chr = $matches[0]; + if (strlen($chr) == 1) { + return sprintf('\\x%02X', ord($chr)); + } + $chr = $this->convertEncoding($chr, 'UTF-16BE', 'UTF-8'); + return sprintf('\\u%04s', strtoupper(bin2hex($chr))); + } + + /** + * Callback function for preg_replace_callback that applies CSS + * escaping to all matches. + * + * @param array $matches + * @return string + */ + protected function cssMatcher($matches) + { + $chr = $matches[0]; + if (strlen($chr) == 1) { + $ord = ord($chr); + } else { + $chr = $this->convertEncoding($chr, 'UTF-16BE', 'UTF-8'); + $ord = hexdec(bin2hex($chr)); + } + return sprintf('\\%X ', $ord); + } + + /** + * Converts a string to UTF-8 from the base encoding. The base encoding is set via this + * class' constructor. + * + * @param string $string + * @throws Exception\RuntimeException + * @return string + */ + protected function toUtf8($string) + { + if ($this->getEncoding() === 'utf-8') { + $result = $string; + } else { + $result = $this->convertEncoding($string, 'UTF-8', $this->getEncoding()); + } + + if (!$this->isUtf8($result)) { + throw new Exception\RuntimeException(sprintf( + 'String to be escaped was not valid UTF-8 or could not be converted: %s', $result + )); + } + + return $result; + } + + /** + * Converts a string from UTF-8 to the base encoding. The base encoding is set via this + * class' constructor. + * @param string $string + * @return string + */ + protected function fromUtf8($string) + { + if ($this->getEncoding() === 'utf-8') { + return $string; + } + + return $this->convertEncoding($string, $this->getEncoding(), 'UTF-8'); + } + + /** + * Checks if a given string appears to be valid UTF-8 or not. + * + * @param string $string + * @return bool + */ + protected function isUtf8($string) + { + return ($string === '' || preg_match('/^./su', $string)); + } + + /** + * Encoding conversion helper which wraps iconv and mbstring where they exist or throws + * and exception where neither is available. + * + * @param string $string + * @param string $to + * @param array|string $from + * @throws Exception\RuntimeException + * @return string + */ + protected function convertEncoding($string, $to, $from) + { + if (function_exists('iconv')) { + $result = iconv($from, $to, $string); + } elseif (function_exists('mb_convert_encoding')) { + $result = mb_convert_encoding($string, $to, $from); + } else { + throw new Exception\RuntimeException( + get_class($this) + . ' requires either the iconv or mbstring extension to be installed' + . ' when escaping for non UTF-8 strings.' + ); + } + + if ($result === false) { + return ''; // return non-fatal blank string on encoding errors from users + } + return $result; + } +} diff --git a/library/Zend/Escaper/Exception/ExceptionInterface.php b/library/Zend/Escaper/Exception/ExceptionInterface.php new file mode 100755 index 0000000000..3a7c8a3f2e --- /dev/null +++ b/library/Zend/Escaper/Exception/ExceptionInterface.php @@ -0,0 +1,14 @@ +=5.3.23" + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev", + "dev-develop": "2.4-dev" + } + } +} diff --git a/library/Zend/EventManager/AbstractListenerAggregate.php b/library/Zend/EventManager/AbstractListenerAggregate.php new file mode 100755 index 0000000000..3f4df278fc --- /dev/null +++ b/library/Zend/EventManager/AbstractListenerAggregate.php @@ -0,0 +1,33 @@ +listeners as $index => $callback) { + if ($events->detach($callback)) { + unset($this->listeners[$index]); + } + } + } +} diff --git a/library/Zend/EventManager/CONTRIBUTING.md b/library/Zend/EventManager/CONTRIBUTING.md new file mode 100755 index 0000000000..e77f5d2d5b --- /dev/null +++ b/library/Zend/EventManager/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# CONTRIBUTING + +Please don't open pull requests against this repository, please use https://github.com/zendframework/zf2. \ No newline at end of file diff --git a/library/Zend/EventManager/Event.php b/library/Zend/EventManager/Event.php new file mode 100755 index 0000000000..abc34e7449 --- /dev/null +++ b/library/Zend/EventManager/Event.php @@ -0,0 +1,209 @@ +setName($name); + } + + if (null !== $target) { + $this->setTarget($target); + } + + if (null !== $params) { + $this->setParams($params); + } + } + + /** + * Get event name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Get the event target + * + * This may be either an object, or the name of a static method. + * + * @return string|object + */ + public function getTarget() + { + return $this->target; + } + + /** + * Set parameters + * + * Overwrites parameters + * + * @param array|ArrayAccess|object $params + * @return Event + * @throws Exception\InvalidArgumentException + */ + public function setParams($params) + { + if (!is_array($params) && !is_object($params)) { + throw new Exception\InvalidArgumentException( + sprintf('Event parameters must be an array or object; received "%s"', gettype($params)) + ); + } + + $this->params = $params; + return $this; + } + + /** + * Get all parameters + * + * @return array|object|ArrayAccess + */ + public function getParams() + { + return $this->params; + } + + /** + * Get an individual parameter + * + * If the parameter does not exist, the $default value will be returned. + * + * @param string|int $name + * @param mixed $default + * @return mixed + */ + public function getParam($name, $default = null) + { + // Check in params that are arrays or implement array access + if (is_array($this->params) || $this->params instanceof ArrayAccess) { + if (!isset($this->params[$name])) { + return $default; + } + + return $this->params[$name]; + } + + // Check in normal objects + if (!isset($this->params->{$name})) { + return $default; + } + return $this->params->{$name}; + } + + /** + * Set the event name + * + * @param string $name + * @return Event + */ + public function setName($name) + { + $this->name = (string) $name; + return $this; + } + + /** + * Set the event target/context + * + * @param null|string|object $target + * @return Event + */ + public function setTarget($target) + { + $this->target = $target; + return $this; + } + + /** + * Set an individual parameter to a value + * + * @param string|int $name + * @param mixed $value + * @return Event + */ + public function setParam($name, $value) + { + if (is_array($this->params) || $this->params instanceof ArrayAccess) { + // Arrays or objects implementing array access + $this->params[$name] = $value; + } else { + // Objects + $this->params->{$name} = $value; + } + return $this; + } + + /** + * Stop further event propagation + * + * @param bool $flag + * @return void + */ + public function stopPropagation($flag = true) + { + $this->stopPropagation = (bool) $flag; + } + + /** + * Is propagation stopped? + * + * @return bool + */ + public function propagationIsStopped() + { + return $this->stopPropagation; + } +} diff --git a/library/Zend/EventManager/EventInterface.php b/library/Zend/EventManager/EventInterface.php new file mode 100755 index 0000000000..c1d0de701d --- /dev/null +++ b/library/Zend/EventManager/EventInterface.php @@ -0,0 +1,96 @@ +setIdentifiers($identifiers); + } + + /** + * Set the event class to utilize + * + * @param string $class + * @return EventManager + */ + public function setEventClass($class) + { + $this->eventClass = $class; + return $this; + } + + /** + * Set shared event manager + * + * @param SharedEventManagerInterface $sharedEventManager + * @return EventManager + */ + public function setSharedManager(SharedEventManagerInterface $sharedEventManager) + { + $this->sharedManager = $sharedEventManager; + StaticEventManager::setInstance($sharedEventManager); + return $this; + } + + /** + * Remove any shared event manager currently attached + * + * @return void + */ + public function unsetSharedManager() + { + $this->sharedManager = false; + } + + /** + * Get shared event manager + * + * If one is not defined, but we have a static instance in + * StaticEventManager, that one will be used and set in this instance. + * + * If none is available in the StaticEventManager, a boolean false is + * returned. + * + * @return false|SharedEventManagerInterface + */ + public function getSharedManager() + { + // "false" means "I do not want a shared manager; don't try and fetch one" + if (false === $this->sharedManager + || $this->sharedManager instanceof SharedEventManagerInterface + ) { + return $this->sharedManager; + } + + if (!StaticEventManager::hasInstance()) { + return false; + } + + $this->sharedManager = StaticEventManager::getInstance(); + return $this->sharedManager; + } + + /** + * Get the identifier(s) for this EventManager + * + * @return array + */ + public function getIdentifiers() + { + return $this->identifiers; + } + + /** + * Set the identifiers (overrides any currently set identifiers) + * + * @param string|int|array|Traversable $identifiers + * @return EventManager Provides a fluent interface + */ + public function setIdentifiers($identifiers) + { + if (is_array($identifiers) || $identifiers instanceof Traversable) { + $this->identifiers = array_unique((array) $identifiers); + } elseif ($identifiers !== null) { + $this->identifiers = array($identifiers); + } + return $this; + } + + /** + * Add some identifier(s) (appends to any currently set identifiers) + * + * @param string|int|array|Traversable $identifiers + * @return EventManager Provides a fluent interface + */ + public function addIdentifiers($identifiers) + { + if (is_array($identifiers) || $identifiers instanceof Traversable) { + $this->identifiers = array_unique(array_merge($this->identifiers, (array) $identifiers)); + } elseif ($identifiers !== null) { + $this->identifiers = array_unique(array_merge($this->identifiers, array($identifiers))); + } + return $this; + } + + /** + * Trigger all listeners for a given event + * + * Can emulate triggerUntil() if the last argument provided is a callback. + * + * @param string $event + * @param string|object $target Object calling emit, or symbol describing target (such as static method name) + * @param array|ArrayAccess $argv Array of arguments; typically, should be associative + * @param null|callable $callback + * @return ResponseCollection All listener return values + * @throws Exception\InvalidCallbackException + */ + public function trigger($event, $target = null, $argv = array(), $callback = null) + { + if ($event instanceof EventInterface) { + $e = $event; + $event = $e->getName(); + $callback = $target; + } elseif ($target instanceof EventInterface) { + $e = $target; + $e->setName($event); + $callback = $argv; + } elseif ($argv instanceof EventInterface) { + $e = $argv; + $e->setName($event); + $e->setTarget($target); + } else { + $e = new $this->eventClass(); + $e->setName($event); + $e->setTarget($target); + $e->setParams($argv); + } + + if ($callback && !is_callable($callback)) { + throw new Exception\InvalidCallbackException('Invalid callback provided'); + } + + // Initial value of stop propagation flag should be false + $e->stopPropagation(false); + + return $this->triggerListeners($event, $e, $callback); + } + + /** + * Trigger listeners until return value of one causes a callback to + * evaluate to true + * + * Triggers listeners until the provided callback evaluates the return + * value of one as true, or until all listeners have been executed. + * + * @param string $event + * @param string|object $target Object calling emit, or symbol describing target (such as static method name) + * @param array|ArrayAccess $argv Array of arguments; typically, should be associative + * @param callable $callback + * @return ResponseCollection + * @throws Exception\InvalidCallbackException if invalid callable provided + */ + public function triggerUntil($event, $target, $argv = null, $callback = null) + { + if ($event instanceof EventInterface) { + $e = $event; + $event = $e->getName(); + $callback = $target; + } elseif ($target instanceof EventInterface) { + $e = $target; + $e->setName($event); + $callback = $argv; + } elseif ($argv instanceof EventInterface) { + $e = $argv; + $e->setName($event); + $e->setTarget($target); + } else { + $e = new $this->eventClass(); + $e->setName($event); + $e->setTarget($target); + $e->setParams($argv); + } + + if (!is_callable($callback)) { + throw new Exception\InvalidCallbackException('Invalid callback provided'); + } + + // Initial value of stop propagation flag should be false + $e->stopPropagation(false); + + return $this->triggerListeners($event, $e, $callback); + } + + /** + * Attach a listener to an event + * + * The first argument is the event, and the next argument describes a + * callback that will respond to that event. A CallbackHandler instance + * describing the event listener combination will be returned. + * + * The last argument indicates a priority at which the event should be + * executed. By default, this value is 1; however, you may set it for any + * integer value. Higher values have higher priority (i.e., execute first). + * + * You can specify "*" for the event name. In such cases, the listener will + * be triggered for every event. + * + * @param string|array|ListenerAggregateInterface $event An event or array of event names. If a ListenerAggregateInterface, proxies to {@link attachAggregate()}. + * @param callable|int $callback If string $event provided, expects PHP callback; for a ListenerAggregateInterface $event, this will be the priority + * @param int $priority If provided, the priority at which to register the callable + * @return CallbackHandler|mixed CallbackHandler if attaching callable (to allow later unsubscribe); mixed if attaching aggregate + * @throws Exception\InvalidArgumentException + */ + public function attach($event, $callback = null, $priority = 1) + { + // Proxy ListenerAggregateInterface arguments to attachAggregate() + if ($event instanceof ListenerAggregateInterface) { + return $this->attachAggregate($event, $callback); + } + + // Null callback is invalid + if (null === $callback) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects a callback; none provided', + __METHOD__ + )); + } + + // Array of events should be registered individually, and return an array of all listeners + if (is_array($event)) { + $listeners = array(); + foreach ($event as $name) { + $listeners[] = $this->attach($name, $callback, $priority); + } + return $listeners; + } + + // If we don't have a priority queue for the event yet, create one + if (empty($this->events[$event])) { + $this->events[$event] = new PriorityQueue(); + } + + // Create a callback handler, setting the event and priority in its metadata + $listener = new CallbackHandler($callback, array('event' => $event, 'priority' => $priority)); + + // Inject the callback handler into the queue + $this->events[$event]->insert($listener, $priority); + return $listener; + } + + /** + * Attach a listener aggregate + * + * Listener aggregates accept an EventManagerInterface instance, and call attach() + * one or more times, typically to attach to multiple events using local + * methods. + * + * @param ListenerAggregateInterface $aggregate + * @param int $priority If provided, a suggested priority for the aggregate to use + * @return mixed return value of {@link ListenerAggregateInterface::attach()} + */ + public function attachAggregate(ListenerAggregateInterface $aggregate, $priority = 1) + { + return $aggregate->attach($this, $priority); + } + + /** + * Unsubscribe a listener from an event + * + * @param CallbackHandler|ListenerAggregateInterface $listener + * @return bool Returns true if event and listener found, and unsubscribed; returns false if either event or listener not found + * @throws Exception\InvalidArgumentException if invalid listener provided + */ + public function detach($listener) + { + if ($listener instanceof ListenerAggregateInterface) { + return $this->detachAggregate($listener); + } + + if (!$listener instanceof CallbackHandler) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expected a ListenerAggregateInterface or CallbackHandler; received "%s"', + __METHOD__, + (is_object($listener) ? get_class($listener) : gettype($listener)) + )); + } + + $event = $listener->getMetadatum('event'); + if (!$event || empty($this->events[$event])) { + return false; + } + $return = $this->events[$event]->remove($listener); + if (!$return) { + return false; + } + if (!count($this->events[$event])) { + unset($this->events[$event]); + } + return true; + } + + /** + * Detach a listener aggregate + * + * Listener aggregates accept an EventManagerInterface instance, and call detach() + * of all previously attached listeners. + * + * @param ListenerAggregateInterface $aggregate + * @return mixed return value of {@link ListenerAggregateInterface::detach()} + */ + public function detachAggregate(ListenerAggregateInterface $aggregate) + { + return $aggregate->detach($this); + } + + /** + * Retrieve all registered events + * + * @return array + */ + public function getEvents() + { + return array_keys($this->events); + } + + /** + * Retrieve all listeners for a given event + * + * @param string $event + * @return PriorityQueue + */ + public function getListeners($event) + { + if (!array_key_exists($event, $this->events)) { + return new PriorityQueue(); + } + return $this->events[$event]; + } + + /** + * Clear all listeners for a given event + * + * @param string $event + * @return void + */ + public function clearListeners($event) + { + if (!empty($this->events[$event])) { + unset($this->events[$event]); + } + } + + /** + * Prepare arguments + * + * Use this method if you want to be able to modify arguments from within a + * listener. It returns an ArrayObject of the arguments, which may then be + * passed to trigger() or triggerUntil(). + * + * @param array $args + * @return ArrayObject + */ + public function prepareArgs(array $args) + { + return new ArrayObject($args); + } + + /** + * Trigger listeners + * + * Actual functionality for triggering listeners, to which both trigger() and triggerUntil() + * delegate. + * + * @param string $event Event name + * @param EventInterface $e + * @param null|callable $callback + * @return ResponseCollection + */ + protected function triggerListeners($event, EventInterface $e, $callback = null) + { + $responses = new ResponseCollection; + $listeners = $this->getListeners($event); + + // Add shared/wildcard listeners to the list of listeners, + // but don't modify the listeners object + $sharedListeners = $this->getSharedListeners($event); + $sharedWildcardListeners = $this->getSharedListeners('*'); + $wildcardListeners = $this->getListeners('*'); + if (count($sharedListeners) || count($sharedWildcardListeners) || count($wildcardListeners)) { + $listeners = clone $listeners; + + // Shared listeners on this specific event + $this->insertListeners($listeners, $sharedListeners); + + // Shared wildcard listeners + $this->insertListeners($listeners, $sharedWildcardListeners); + + // Add wildcard listeners + $this->insertListeners($listeners, $wildcardListeners); + } + + foreach ($listeners as $listener) { + $listenerCallback = $listener->getCallback(); + + // Trigger the listener's callback, and push its result onto the + // response collection + $responses->push(call_user_func($listenerCallback, $e)); + + // If the event was asked to stop propagating, do so + if ($e->propagationIsStopped()) { + $responses->setStopped(true); + break; + } + + // If the result causes our validation callback to return true, + // stop propagation + if ($callback && call_user_func($callback, $responses->last())) { + $responses->setStopped(true); + break; + } + } + + return $responses; + } + + /** + * Get list of all listeners attached to the shared event manager for + * identifiers registered by this instance + * + * @param string $event + * @return array + */ + protected function getSharedListeners($event) + { + if (!$sharedManager = $this->getSharedManager()) { + return array(); + } + + $identifiers = $this->getIdentifiers(); + //Add wildcard id to the search, if not already added + if (!in_array('*', $identifiers)) { + $identifiers[] = '*'; + } + $sharedListeners = array(); + + foreach ($identifiers as $id) { + if (!$listeners = $sharedManager->getListeners($id, $event)) { + continue; + } + + if (!is_array($listeners) && !($listeners instanceof Traversable)) { + continue; + } + + foreach ($listeners as $listener) { + if (!$listener instanceof CallbackHandler) { + continue; + } + $sharedListeners[] = $listener; + } + } + + return $sharedListeners; + } + + /** + * Add listeners to the master queue of listeners + * + * Used to inject shared listeners and wildcard listeners. + * + * @param PriorityQueue $masterListeners + * @param PriorityQueue $listeners + * @return void + */ + protected function insertListeners($masterListeners, $listeners) + { + foreach ($listeners as $listener) { + $priority = $listener->getMetadatum('priority'); + if (null === $priority) { + $priority = 1; + } elseif (is_array($priority)) { + // If we have an array, likely using PriorityQueue. Grab first + // element of the array, as that's the actual priority. + $priority = array_shift($priority); + } + $masterListeners->insert($listener, $priority); + } + } +} diff --git a/library/Zend/EventManager/EventManagerAwareInterface.php b/library/Zend/EventManager/EventManagerAwareInterface.php new file mode 100755 index 0000000000..a5c25f25fa --- /dev/null +++ b/library/Zend/EventManager/EventManagerAwareInterface.php @@ -0,0 +1,24 @@ +eventIdentifier property. + * + * @param EventManagerInterface $events + * @return mixed + */ + public function setEventManager(EventManagerInterface $events) + { + $identifiers = array(__CLASS__, get_class($this)); + if (isset($this->eventIdentifier)) { + if ((is_string($this->eventIdentifier)) + || (is_array($this->eventIdentifier)) + || ($this->eventIdentifier instanceof Traversable) + ) { + $identifiers = array_unique(array_merge($identifiers, (array) $this->eventIdentifier)); + } elseif (is_object($this->eventIdentifier)) { + $identifiers[] = $this->eventIdentifier; + } + // silently ignore invalid eventIdentifier types + } + $events->setIdentifiers($identifiers); + $this->events = $events; + if (method_exists($this, 'attachDefaultListeners')) { + $this->attachDefaultListeners(); + } + return $this; + } + + /** + * Retrieve the event manager + * + * Lazy-loads an EventManager instance if none registered. + * + * @return EventManagerInterface + */ + public function getEventManager() + { + if (!$this->events instanceof EventManagerInterface) { + $this->setEventManager(new EventManager()); + } + return $this->events; + } +} diff --git a/library/Zend/EventManager/EventManagerInterface.php b/library/Zend/EventManager/EventManagerInterface.php new file mode 100755 index 0000000000..6a2129f93a --- /dev/null +++ b/library/Zend/EventManager/EventManagerInterface.php @@ -0,0 +1,144 @@ +setExtractFlags(self::EXTR_BOTH); + + // Iterate and remove any matches + $removed = false; + $items = array(); + $this->rewind(); + while (!$this->isEmpty()) { + $item = $this->extract(); + if ($item['data'] === $datum) { + $removed = true; + continue; + } + $items[] = $item; + } + + // Repopulate + foreach ($items as $item) { + $this->insert($item['data'], $item['priority']); + } + + $this->setExtractFlags(self::EXTR_DATA); + return $removed; + } + + /** + * Iterate the next filter in the chain + * + * Iterates and calls the next filter in the chain. + * + * @param mixed $context + * @param array $params + * @param FilterIterator $chain + * @return mixed + */ + public function next($context = null, array $params = array(), $chain = null) + { + if (empty($context) || $chain->isEmpty()) { + return; + } + + $next = $this->extract(); + if (!$next instanceof CallbackHandler) { + return; + } + + $return = call_user_func($next->getCallback(), $context, $params, $chain); + return $return; + } +} diff --git a/library/Zend/EventManager/FilterChain.php b/library/Zend/EventManager/FilterChain.php new file mode 100755 index 0000000000..d79a5de97c --- /dev/null +++ b/library/Zend/EventManager/FilterChain.php @@ -0,0 +1,120 @@ +filters = new Filter\FilterIterator(); + } + + /** + * Apply the filters + * + * Begins iteration of the filters. + * + * @param mixed $context Object under observation + * @param mixed $argv Associative array of arguments + * @return mixed + */ + public function run($context, array $argv = array()) + { + $chain = clone $this->getFilters(); + + if ($chain->isEmpty()) { + return; + } + + $next = $chain->extract(); + if (!$next instanceof CallbackHandler) { + return; + } + + return call_user_func($next->getCallback(), $context, $argv, $chain); + } + + /** + * Connect a filter to the chain + * + * @param callable $callback PHP Callback + * @param int $priority Priority in the queue at which to execute; defaults to 1 (higher numbers == higher priority) + * @return CallbackHandler (to allow later unsubscribe) + * @throws Exception\InvalidCallbackException + */ + public function attach($callback, $priority = 1) + { + if (empty($callback)) { + throw new Exception\InvalidCallbackException('No callback provided'); + } + $filter = new CallbackHandler($callback, array('priority' => $priority)); + $this->filters->insert($filter, $priority); + return $filter; + } + + /** + * Detach a filter from the chain + * + * @param CallbackHandler $filter + * @return bool Returns true if filter found and unsubscribed; returns false otherwise + */ + public function detach(CallbackHandler $filter) + { + return $this->filters->remove($filter); + } + + /** + * Retrieve all filters + * + * @return Filter\FilterIterator + */ + public function getFilters() + { + return $this->filters; + } + + /** + * Clear all filters + * + * @return void + */ + public function clearFilters() + { + $this->filters = new Filter\FilterIterator(); + } + + /** + * Return current responses + * + * Only available while the chain is still being iterated. Returns the + * current ResponseCollection. + * + * @return null|ResponseCollection + */ + public function getResponses() + { + return null; + } +} diff --git a/library/Zend/EventManager/GlobalEventManager.php b/library/Zend/EventManager/GlobalEventManager.php new file mode 100755 index 0000000000..4bac5b5741 --- /dev/null +++ b/library/Zend/EventManager/GlobalEventManager.php @@ -0,0 +1,135 @@ +trigger($event, $context, $argv); + } + + /** + * Trigger listeners until return value of one causes a callback to evaluate + * to true. + * + * @param string $event + * @param string|object $context + * @param array|object $argv + * @param callable $callback + * @return ResponseCollection + */ + public static function triggerUntil($event, $context, $argv, $callback) + { + return static::getEventCollection()->triggerUntil($event, $context, $argv, $callback); + } + + /** + * Attach a listener to an event + * + * @param string $event + * @param callable $callback + * @param int $priority + * @return CallbackHandler + */ + public static function attach($event, $callback, $priority = 1) + { + return static::getEventCollection()->attach($event, $callback, $priority); + } + + /** + * Detach a callback from a listener + * + * @param CallbackHandler $listener + * @return bool + */ + public static function detach(CallbackHandler $listener) + { + return static::getEventCollection()->detach($listener); + } + + /** + * Retrieve list of events this object manages + * + * @return array + */ + public static function getEvents() + { + return static::getEventCollection()->getEvents(); + } + + /** + * Retrieve all listeners for a given event + * + * @param string $event + * @return PriorityQueue|array + */ + public static function getListeners($event) + { + return static::getEventCollection()->getListeners($event); + } + + /** + * Clear all listeners for a given event + * + * @param string $event + * @return void + */ + public static function clearListeners($event) + { + static::getEventCollection()->clearListeners($event); + } +} diff --git a/library/Zend/EventManager/ListenerAggregateInterface.php b/library/Zend/EventManager/ListenerAggregateInterface.php new file mode 100755 index 0000000000..cd0eef4ce5 --- /dev/null +++ b/library/Zend/EventManager/ListenerAggregateInterface.php @@ -0,0 +1,42 @@ +listeners as $index => $callback) { + if ($events->detach($callback)) { + unset($this->listeners[$index]); + } + } + } +} diff --git a/library/Zend/EventManager/ProvidesEvents.php b/library/Zend/EventManager/ProvidesEvents.php new file mode 100755 index 0000000000..0cfeb19753 --- /dev/null +++ b/library/Zend/EventManager/ProvidesEvents.php @@ -0,0 +1,23 @@ +stopped; + } + + /** + * Mark the collection as stopped (or its opposite) + * + * @param bool $flag + * @return ResponseCollection + */ + public function setStopped($flag) + { + $this->stopped = (bool) $flag; + return $this; + } + + /** + * Convenient access to the first handler return value. + * + * @return mixed The first handler return value + */ + public function first() + { + return parent::bottom(); + } + + /** + * Convenient access to the last handler return value. + * + * If the collection is empty, returns null. Otherwise, returns value + * returned by last handler. + * + * @return mixed The last handler return value + */ + public function last() + { + if (count($this) === 0) { + return null; + } + return parent::top(); + } + + /** + * Check if any of the responses match the given value. + * + * @param mixed $value The value to look for among responses + * @return bool + */ + public function contains($value) + { + foreach ($this as $response) { + if ($response === $value) { + return true; + } + } + return false; + } +} diff --git a/library/Zend/EventManager/SharedEventAggregateAwareInterface.php b/library/Zend/EventManager/SharedEventAggregateAwareInterface.php new file mode 100755 index 0000000000..4cda8bc12b --- /dev/null +++ b/library/Zend/EventManager/SharedEventAggregateAwareInterface.php @@ -0,0 +1,33 @@ + + * $sharedEventManager = new SharedEventManager(); + * $sharedEventManager->attach( + * array('My\Resource\AbstractResource', 'My\Resource\EntityResource'), + * 'getAll', + * function ($e) use ($cache) { + * if (!$id = $e->getParam('id', false)) { + * return; + * } + * if (!$data = $cache->load(get_class($resource) . '::getOne::' . $id )) { + * return; + * } + * return $data; + * } + * ); + * + * + * @param string|array $id Identifier(s) for event emitting component(s) + * @param string $event + * @param callable $callback PHP Callback + * @param int $priority Priority at which listener should execute + * @return CallbackHandler|array Either CallbackHandler or array of CallbackHandlers + */ + public function attach($id, $event, $callback, $priority = 1) + { + $ids = (array) $id; + $listeners = array(); + foreach ($ids as $id) { + if (!array_key_exists($id, $this->identifiers)) { + $this->identifiers[$id] = new EventManager($id); + } + $listeners[] = $this->identifiers[$id]->attach($event, $callback, $priority); + } + if (count($listeners) > 1) { + return $listeners; + } + return $listeners[0]; + } + + /** + * Attach a listener aggregate + * + * Listener aggregates accept an EventManagerInterface instance, and call attachShared() + * one or more times, typically to attach to multiple events using local + * methods. + * + * @param SharedListenerAggregateInterface $aggregate + * @param int $priority If provided, a suggested priority for the aggregate to use + * @return mixed return value of {@link ListenerAggregateInterface::attachShared()} + */ + public function attachAggregate(SharedListenerAggregateInterface $aggregate, $priority = 1) + { + return $aggregate->attachShared($this, $priority); + } + + /** + * Detach a listener from an event offered by a given resource + * + * @param string|int $id + * @param CallbackHandler $listener + * @return bool Returns true if event and listener found, and unsubscribed; returns false if either event or listener not found + */ + public function detach($id, CallbackHandler $listener) + { + if (!array_key_exists($id, $this->identifiers)) { + return false; + } + return $this->identifiers[$id]->detach($listener); + } + + /** + * Detach a listener aggregate + * + * Listener aggregates accept a SharedEventManagerInterface instance, and call detachShared() + * of all previously attached listeners. + * + * @param SharedListenerAggregateInterface $aggregate + * @return mixed return value of {@link SharedListenerAggregateInterface::detachShared()} + */ + public function detachAggregate(SharedListenerAggregateInterface $aggregate) + { + return $aggregate->detachShared($this); + } + + /** + * Retrieve all registered events for a given resource + * + * @param string|int $id + * @return array + */ + public function getEvents($id) + { + if (!array_key_exists($id, $this->identifiers)) { + //Check if there are any id wildcards listeners + if ('*' != $id && array_key_exists('*', $this->identifiers)) { + return $this->identifiers['*']->getEvents(); + } + return false; + } + return $this->identifiers[$id]->getEvents(); + } + + /** + * Retrieve all listeners for a given identifier and event + * + * @param string|int $id + * @param string|int $event + * @return false|PriorityQueue + */ + public function getListeners($id, $event) + { + if (!array_key_exists($id, $this->identifiers)) { + return false; + } + return $this->identifiers[$id]->getListeners($event); + } + + /** + * Clear all listeners for a given identifier, optionally for a specific event + * + * @param string|int $id + * @param null|string $event + * @return bool + */ + public function clearListeners($id, $event = null) + { + if (!array_key_exists($id, $this->identifiers)) { + return false; + } + + if (null === $event) { + unset($this->identifiers[$id]); + return true; + } + + return $this->identifiers[$id]->clearListeners($event); + } +} diff --git a/library/Zend/EventManager/SharedEventManagerAwareInterface.php b/library/Zend/EventManager/SharedEventManagerAwareInterface.php new file mode 100755 index 0000000000..09e5c98c12 --- /dev/null +++ b/library/Zend/EventManager/SharedEventManagerAwareInterface.php @@ -0,0 +1,38 @@ +=5.3.23", + "zendframework/zend-stdlib": "self.version" + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev", + "dev-develop": "2.4-dev" + } + } +} diff --git a/library/Zend/Feed/CONTRIBUTING.md b/library/Zend/Feed/CONTRIBUTING.md new file mode 100755 index 0000000000..e77f5d2d5b --- /dev/null +++ b/library/Zend/Feed/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# CONTRIBUTING + +Please don't open pull requests against this repository, please use https://github.com/zendframework/zf2. \ No newline at end of file diff --git a/library/Zend/Feed/Exception/BadMethodCallException.php b/library/Zend/Feed/Exception/BadMethodCallException.php new file mode 100755 index 0000000000..107c3e64ef --- /dev/null +++ b/library/Zend/Feed/Exception/BadMethodCallException.php @@ -0,0 +1,16 @@ +setOptions($options); + } + } + + /** + * Process any injected configuration options + * + * @param array|Traversable $options Options array or Traversable object + * @return AbstractCallback + * @throws Exception\InvalidArgumentException + */ + public function setOptions($options) + { + if ($options instanceof Traversable) { + $options = ArrayUtils::iteratorToArray($options); + } + + if (!is_array($options)) { + throw new Exception\InvalidArgumentException('Array or Traversable object' + . 'expected, got ' . gettype($options)); + } + + if (is_array($options)) { + $this->setOptions($options); + } + + if (array_key_exists('storage', $options)) { + $this->setStorage($options['storage']); + } + return $this; + } + + /** + * Send the response, including all headers. + * If you wish to handle this via Zend\Http, use the getter methods + * to retrieve any data needed to be set on your HTTP Response object, or + * simply give this object the HTTP Response instance to work with for you! + * + * @return void + */ + public function sendResponse() + { + $this->getHttpResponse()->send(); + } + + /** + * Sets an instance of Zend\Feed\Pubsubhubbub\Model\SubscriptionPersistence used + * to background save any verification tokens associated with a subscription + * or other. + * + * @param Model\SubscriptionPersistenceInterface $storage + * @return AbstractCallback + */ + public function setStorage(Model\SubscriptionPersistenceInterface $storage) + { + $this->storage = $storage; + return $this; + } + + /** + * Gets an instance of Zend\Feed\Pubsubhubbub\Model\SubscriptionPersistence used + * to background save any verification tokens associated with a subscription + * or other. + * + * @return Model\SubscriptionPersistenceInterface + * @throws Exception\RuntimeException + */ + public function getStorage() + { + if ($this->storage === null) { + throw new Exception\RuntimeException('No storage object has been' + . ' set that subclasses Zend\Feed\Pubsubhubbub\Model\SubscriptionPersistence'); + } + return $this->storage; + } + + /** + * An instance of a class handling Http Responses. This is implemented in + * Zend\Feed\Pubsubhubbub\HttpResponse which shares an unenforced interface with + * (i.e. not inherited from) Zend\Controller\Response\Http. + * + * @param HttpResponse|PhpResponse $httpResponse + * @return AbstractCallback + * @throws Exception\InvalidArgumentException + */ + public function setHttpResponse($httpResponse) + { + if (!$httpResponse instanceof HttpResponse && !$httpResponse instanceof PhpResponse) { + throw new Exception\InvalidArgumentException('HTTP Response object must' + . ' implement one of Zend\Feed\Pubsubhubbub\HttpResponse or' + . ' Zend\Http\PhpEnvironment\Response'); + } + $this->httpResponse = $httpResponse; + return $this; + } + + /** + * An instance of a class handling Http Responses. This is implemented in + * Zend\Feed\Pubsubhubbub\HttpResponse which shares an unenforced interface with + * (i.e. not inherited from) Zend\Controller\Response\Http. + * + * @return HttpResponse|PhpResponse + */ + public function getHttpResponse() + { + if ($this->httpResponse === null) { + $this->httpResponse = new HttpResponse; + } + return $this->httpResponse; + } + + /** + * Sets the number of Subscribers for which any updates are on behalf of. + * In other words, is this class serving one or more subscribers? How many? + * Defaults to 1 if left unchanged. + * + * @param string|int $count + * @return AbstractCallback + * @throws Exception\InvalidArgumentException + */ + public function setSubscriberCount($count) + { + $count = intval($count); + if ($count <= 0) { + throw new Exception\InvalidArgumentException('Subscriber count must be' + . ' greater than zero'); + } + $this->subscriberCount = $count; + return $this; + } + + /** + * Gets the number of Subscribers for which any updates are on behalf of. + * In other words, is this class serving one or more subscribers? How many? + * + * @return int + */ + public function getSubscriberCount() + { + return $this->subscriberCount; + } + + /** + * Attempt to detect the callback URL (specifically the path forward) + * @return string + */ + protected function _detectCallbackUrl() + { + $callbackUrl = ''; + if (isset($_SERVER['HTTP_X_ORIGINAL_URL'])) { + $callbackUrl = $_SERVER['HTTP_X_ORIGINAL_URL']; + } elseif (isset($_SERVER['HTTP_X_REWRITE_URL'])) { + $callbackUrl = $_SERVER['HTTP_X_REWRITE_URL']; + } elseif (isset($_SERVER['REQUEST_URI'])) { + $callbackUrl = $_SERVER['REQUEST_URI']; + $scheme = 'http'; + if ($_SERVER['HTTPS'] == 'on') { + $scheme = 'https'; + } + $schemeAndHttpHost = $scheme . '://' . $this->_getHttpHost(); + if (strpos($callbackUrl, $schemeAndHttpHost) === 0) { + $callbackUrl = substr($callbackUrl, strlen($schemeAndHttpHost)); + } + } elseif (isset($_SERVER['ORIG_PATH_INFO'])) { + $callbackUrl= $_SERVER['ORIG_PATH_INFO']; + if (!empty($_SERVER['QUERY_STRING'])) { + $callbackUrl .= '?' . $_SERVER['QUERY_STRING']; + } + } + return $callbackUrl; + } + + /** + * Get the HTTP host + * + * @return string + */ + protected function _getHttpHost() + { + if (!empty($_SERVER['HTTP_HOST'])) { + return $_SERVER['HTTP_HOST']; + } + $scheme = 'http'; + if ($_SERVER['HTTPS'] == 'on') { + $scheme = 'https'; + } + $name = $_SERVER['SERVER_NAME']; + $port = $_SERVER['SERVER_PORT']; + if (($scheme == 'http' && $port == 80) + || ($scheme == 'https' && $port == 443) + ) { + return $name; + } + + return $name . ':' . $port; + } + + /** + * Retrieve a Header value from either $_SERVER or Apache + * + * @param string $header + * @return bool|string + */ + protected function _getHeader($header) + { + $temp = strtoupper(str_replace('-', '_', $header)); + if (!empty($_SERVER[$temp])) { + return $_SERVER[$temp]; + } + $temp = 'HTTP_' . strtoupper(str_replace('-', '_', $header)); + if (!empty($_SERVER[$temp])) { + return $_SERVER[$temp]; + } + if (function_exists('apache_request_headers')) { + $headers = apache_request_headers(); + if (!empty($headers[$header])) { + return $headers[$header]; + } + } + return false; + } + + /** + * Return the raw body of the request + * + * @return string|false Raw body, or false if not present + */ + protected function _getRawBody() + { + $body = file_get_contents('php://input'); + if (strlen(trim($body)) == 0 && isset($GLOBALS['HTTP_RAW_POST_DATA'])) { + $body = $GLOBALS['HTTP_RAW_POST_DATA']; + } + if (strlen(trim($body)) > 0) { + return $body; + } + return false; + } +} diff --git a/library/Zend/Feed/PubSubHubbub/CallbackInterface.php b/library/Zend/Feed/PubSubHubbub/CallbackInterface.php new file mode 100755 index 0000000000..8873c3db48 --- /dev/null +++ b/library/Zend/Feed/PubSubHubbub/CallbackInterface.php @@ -0,0 +1,51 @@ +sendHeaders(); + echo $this->getContent(); + } + + /** + * Send all headers + * + * Sends any headers specified. If an {@link setHttpResponseCode() HTTP response code} + * has been specified, it is sent with the first header. + * + * @return void + */ + public function sendHeaders() + { + if (count($this->headers) || (200 != $this->statusCode)) { + $this->canSendHeaders(true); + } elseif (200 == $this->statusCode) { + return; + } + $httpCodeSent = false; + foreach ($this->headers as $header) { + if (!$httpCodeSent && $this->statusCode) { + header($header['name'] . ': ' . $header['value'], $header['replace'], $this->statusCode); + $httpCodeSent = true; + } else { + header($header['name'] . ': ' . $header['value'], $header['replace']); + } + } + if (!$httpCodeSent) { + header('HTTP/1.1 ' . $this->statusCode); + } + } + + /** + * Set a header + * + * If $replace is true, replaces any headers already defined with that + * $name. + * + * @param string $name + * @param string $value + * @param bool $replace + * @return \Zend\Feed\PubSubHubbub\HttpResponse + */ + public function setHeader($name, $value, $replace = false) + { + $name = $this->_normalizeHeader($name); + $value = (string) $value; + if ($replace) { + foreach ($this->headers as $key => $header) { + if ($name == $header['name']) { + unset($this->headers[$key]); + } + } + } + $this->headers[] = array( + 'name' => $name, + 'value' => $value, + 'replace' => $replace, + ); + + return $this; + } + + /** + * Check if a specific Header is set and return its value + * + * @param string $name + * @return string|null + */ + public function getHeader($name) + { + $name = $this->_normalizeHeader($name); + foreach ($this->headers as $header) { + if ($header['name'] == $name) { + return $header['value']; + } + } + } + + /** + * Return array of headers; see {@link $headers} for format + * + * @return array + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * Can we send headers? + * + * @param bool $throw Whether or not to throw an exception if headers have been sent; defaults to false + * @return HttpResponse + * @throws Exception\RuntimeException + */ + public function canSendHeaders($throw = false) + { + $ok = headers_sent($file, $line); + if ($ok && $throw) { + throw new Exception\RuntimeException('Cannot send headers; headers already sent in ' . $file . ', line ' . $line); + } + return !$ok; + } + + /** + * Set HTTP response code to use with headers + * + * @param int $code + * @return HttpResponse + * @throws Exception\InvalidArgumentException + */ + public function setStatusCode($code) + { + if (!is_int($code) || (100 > $code) || (599 < $code)) { + throw new Exception\InvalidArgumentException('Invalid HTTP response' + . ' code:' . $code); + } + $this->statusCode = $code; + return $this; + } + + /** + * Retrieve HTTP response code + * + * @return int + */ + public function getStatusCode() + { + return $this->statusCode; + } + + /** + * Set body content + * + * @param string $content + * @return \Zend\Feed\PubSubHubbub\HttpResponse + */ + public function setContent($content) + { + $this->content = (string) $content; + $this->setHeader('content-length', strlen($content)); + return $this; + } + + /** + * Return the body content + * + * @return string + */ + public function getContent() + { + return $this->content; + } + + /** + * Normalizes a header name to X-Capitalized-Names + * + * @param string $name + * @return string + */ + protected function _normalizeHeader($name) + { + $filtered = str_replace(array('-', '_'), ' ', (string) $name); + $filtered = ucwords(strtolower($filtered)); + $filtered = str_replace(' ', '-', $filtered); + return $filtered; + } +} diff --git a/library/Zend/Feed/PubSubHubbub/Model/AbstractModel.php b/library/Zend/Feed/PubSubHubbub/Model/AbstractModel.php new file mode 100755 index 0000000000..92e688133e --- /dev/null +++ b/library/Zend/Feed/PubSubHubbub/Model/AbstractModel.php @@ -0,0 +1,39 @@ +db = new TableGateway($table, null); + } else { + $this->db = $tableGateway; + } + } +} diff --git a/library/Zend/Feed/PubSubHubbub/Model/Subscription.php b/library/Zend/Feed/PubSubHubbub/Model/Subscription.php new file mode 100755 index 0000000000..9571106a40 --- /dev/null +++ b/library/Zend/Feed/PubSubHubbub/Model/Subscription.php @@ -0,0 +1,142 @@ +db->select(array('id' => $data['id'])); + if ($result && (0 < count($result))) { + $data['created_time'] = $result->current()->created_time; + $now = $this->getNow(); + if (array_key_exists('lease_seconds', $data) + && $data['lease_seconds'] + ) { + $data['expiration_time'] = $now->add(new DateInterval('PT' . $data['lease_seconds'] . 'S')) + ->format('Y-m-d H:i:s'); + } + $this->db->update( + $data, + array('id' => $data['id']) + ); + return false; + } + + $this->db->insert($data); + return true; + } + + /** + * Get subscription by ID/key + * + * @param string $key + * @return array + * @throws PubSubHubbub\Exception\InvalidArgumentException + */ + public function getSubscription($key) + { + if (empty($key) || !is_string($key)) { + throw new PubSubHubbub\Exception\InvalidArgumentException('Invalid parameter "key"' + .' of "' . $key . '" must be a non-empty string'); + } + $result = $this->db->select(array('id' => $key)); + if (count($result)) { + return $result->current()->getArrayCopy(); + } + return false; + } + + /** + * Determine if a subscription matching the key exists + * + * @param string $key + * @return bool + * @throws PubSubHubbub\Exception\InvalidArgumentException + */ + public function hasSubscription($key) + { + if (empty($key) || !is_string($key)) { + throw new PubSubHubbub\Exception\InvalidArgumentException('Invalid parameter "key"' + .' of "' . $key . '" must be a non-empty string'); + } + $result = $this->db->select(array('id' => $key)); + if (count($result)) { + return true; + } + return false; + } + + /** + * Delete a subscription + * + * @param string $key + * @return bool + */ + public function deleteSubscription($key) + { + $result = $this->db->select(array('id' => $key)); + if (count($result)) { + $this->db->delete( + array('id' => $key) + ); + return true; + } + return false; + } + + /** + * Get a new DateTime or the one injected for testing + * + * @return DateTime + */ + public function getNow() + { + if (null === $this->now) { + return new DateTime(); + } + return $this->now; + } + + /** + * Set a DateTime instance for assisting with unit testing + * + * @param DateTime $now + * @return Subscription + */ + public function setNow(DateTime $now) + { + $this->now = $now; + return $this; + } +} diff --git a/library/Zend/Feed/PubSubHubbub/Model/SubscriptionPersistenceInterface.php b/library/Zend/Feed/PubSubHubbub/Model/SubscriptionPersistenceInterface.php new file mode 100755 index 0000000000..ccd272329e --- /dev/null +++ b/library/Zend/Feed/PubSubHubbub/Model/SubscriptionPersistenceInterface.php @@ -0,0 +1,45 @@ +getHubs(); + } + + /** + * Allows the external environment to make ZendOAuth use a specific + * Client instance. + * + * @param Http\Client $httpClient + * @return void + */ + public static function setHttpClient(Http\Client $httpClient) + { + static::$httpClient = $httpClient; + } + + /** + * Return the singleton instance of the HTTP Client. Note that + * the instance is reset and cleared of previous parameters GET/POST. + * Headers are NOT reset but handled by this component if applicable. + * + * @return Http\Client + */ + public static function getHttpClient() + { + if (!isset(static::$httpClient)) { + static::$httpClient = new Http\Client; + } else { + static::$httpClient->resetParameters(); + } + return static::$httpClient; + } + + /** + * Simple mechanism to delete the entire singleton HTTP Client instance + * which forces a new instantiation for subsequent requests. + * + * @return void + */ + public static function clearHttpClient() + { + static::$httpClient = null; + } + + /** + * Set the Escaper instance + * + * If null, resets the instance + * + * @param null|Escaper $escaper + */ + public static function setEscaper(Escaper $escaper = null) + { + static::$escaper = $escaper; + } + + /** + * Get the Escaper instance + * + * If none registered, lazy-loads an instance. + * + * @return Escaper + */ + public static function getEscaper() + { + if (null === static::$escaper) { + static::setEscaper(new Escaper()); + } + return static::$escaper; + } + + /** + * RFC 3986 safe url encoding method + * + * @param string $string + * @return string + */ + public static function urlencode($string) + { + $escaper = static::getEscaper(); + $rawencoded = $escaper->escapeUrl($string); + $rfcencoded = str_replace('%7E', '~', $rawencoded); + return $rfcencoded; + } +} diff --git a/library/Zend/Feed/PubSubHubbub/Publisher.php b/library/Zend/Feed/PubSubHubbub/Publisher.php new file mode 100755 index 0000000000..916ffcad59 --- /dev/null +++ b/library/Zend/Feed/PubSubHubbub/Publisher.php @@ -0,0 +1,397 @@ +setOptions($options); + } + } + + /** + * Process any injected configuration options + * + * @param array|Traversable $options Options array or Traversable object + * @return Publisher + * @throws Exception\InvalidArgumentException + */ + public function setOptions($options) + { + if ($options instanceof Traversable) { + $options = ArrayUtils::iteratorToArray($options); + } + + if (!is_array($options)) { + throw new Exception\InvalidArgumentException('Array or Traversable object' + . 'expected, got ' . gettype($options)); + } + if (array_key_exists('hubUrls', $options)) { + $this->addHubUrls($options['hubUrls']); + } + if (array_key_exists('updatedTopicUrls', $options)) { + $this->addUpdatedTopicUrls($options['updatedTopicUrls']); + } + if (array_key_exists('parameters', $options)) { + $this->setParameters($options['parameters']); + } + return $this; + } + + /** + * Add a Hub Server URL supported by Publisher + * + * @param string $url + * @return Publisher + * @throws Exception\InvalidArgumentException + */ + public function addHubUrl($url) + { + if (empty($url) || !is_string($url) || !Uri::factory($url)->isValid()) { + throw new Exception\InvalidArgumentException('Invalid parameter "url"' + . ' of "' . $url . '" must be a non-empty string and a valid' + . 'URL'); + } + $this->hubUrls[] = $url; + return $this; + } + + /** + * Add an array of Hub Server URLs supported by Publisher + * + * @param array $urls + * @return Publisher + */ + public function addHubUrls(array $urls) + { + foreach ($urls as $url) { + $this->addHubUrl($url); + } + return $this; + } + + /** + * Remove a Hub Server URL + * + * @param string $url + * @return Publisher + */ + public function removeHubUrl($url) + { + if (!in_array($url, $this->getHubUrls())) { + return $this; + } + $key = array_search($url, $this->hubUrls); + unset($this->hubUrls[$key]); + return $this; + } + + /** + * Return an array of unique Hub Server URLs currently available + * + * @return array + */ + public function getHubUrls() + { + $this->hubUrls = array_unique($this->hubUrls); + return $this->hubUrls; + } + + /** + * Add a URL to a topic (Atom or RSS feed) which has been updated + * + * @param string $url + * @return Publisher + * @throws Exception\InvalidArgumentException + */ + public function addUpdatedTopicUrl($url) + { + if (empty($url) || !is_string($url) || !Uri::factory($url)->isValid()) { + throw new Exception\InvalidArgumentException('Invalid parameter "url"' + . ' of "' . $url . '" must be a non-empty string and a valid' + . 'URL'); + } + $this->updatedTopicUrls[] = $url; + return $this; + } + + /** + * Add an array of Topic URLs which have been updated + * + * @param array $urls + * @return Publisher + */ + public function addUpdatedTopicUrls(array $urls) + { + foreach ($urls as $url) { + $this->addUpdatedTopicUrl($url); + } + return $this; + } + + /** + * Remove an updated topic URL + * + * @param string $url + * @return Publisher + */ + public function removeUpdatedTopicUrl($url) + { + if (!in_array($url, $this->getUpdatedTopicUrls())) { + return $this; + } + $key = array_search($url, $this->updatedTopicUrls); + unset($this->updatedTopicUrls[$key]); + return $this; + } + + /** + * Return an array of unique updated topic URLs currently available + * + * @return array + */ + public function getUpdatedTopicUrls() + { + $this->updatedTopicUrls = array_unique($this->updatedTopicUrls); + return $this->updatedTopicUrls; + } + + /** + * Notifies a single Hub Server URL of changes + * + * @param string $url The Hub Server's URL + * @return void + * @throws Exception\InvalidArgumentException + * @throws Exception\RuntimeException + */ + public function notifyHub($url) + { + if (empty($url) || !is_string($url) || !Uri::factory($url)->isValid()) { + throw new Exception\InvalidArgumentException('Invalid parameter "url"' + . ' of "' . $url . '" must be a non-empty string and a valid' + . 'URL'); + } + $client = $this->_getHttpClient(); + $client->setUri($url); + $response = $client->getResponse(); + if ($response->getStatusCode() !== 204) { + throw new Exception\RuntimeException('Notification to Hub Server ' + . 'at "' . $url . '" appears to have failed with a status code of "' + . $response->getStatusCode() . '" and message "' + . $response->getContent() . '"'); + } + } + + /** + * Notifies all Hub Server URLs of changes + * + * If a Hub notification fails, certain data will be retained in an + * an array retrieved using getErrors(), if a failure occurs for any Hubs + * the isSuccess() check will return FALSE. This method is designed not + * to needlessly fail with an Exception/Error unless from Zend\Http\Client. + * + * @return void + * @throws Exception\RuntimeException + */ + public function notifyAll() + { + $client = $this->_getHttpClient(); + $hubs = $this->getHubUrls(); + if (empty($hubs)) { + throw new Exception\RuntimeException('No Hub Server URLs' + . ' have been set so no notifications can be sent'); + } + $this->errors = array(); + foreach ($hubs as $url) { + $client->setUri($url); + $response = $client->getResponse(); + if ($response->getStatusCode() !== 204) { + $this->errors[] = array( + 'response' => $response, + 'hubUrl' => $url + ); + } + } + } + + /** + * Add an optional parameter to the update notification requests + * + * @param string $name + * @param string|null $value + * @return Publisher + * @throws Exception\InvalidArgumentException + */ + public function setParameter($name, $value = null) + { + if (is_array($name)) { + $this->setParameters($name); + return $this; + } + if (empty($name) || !is_string($name)) { + throw new Exception\InvalidArgumentException('Invalid parameter "name"' + . ' of "' . $name . '" must be a non-empty string'); + } + if ($value === null) { + $this->removeParameter($name); + return $this; + } + if (empty($value) || (!is_string($value) && $value !== null)) { + throw new Exception\InvalidArgumentException('Invalid parameter "value"' + . ' of "' . $value . '" must be a non-empty string'); + } + $this->parameters[$name] = $value; + return $this; + } + + /** + * Add an optional parameter to the update notification requests + * + * @param array $parameters + * @return Publisher + */ + public function setParameters(array $parameters) + { + foreach ($parameters as $name => $value) { + $this->setParameter($name, $value); + } + return $this; + } + + /** + * Remove an optional parameter for the notification requests + * + * @param string $name + * @return Publisher + * @throws Exception\InvalidArgumentException + */ + public function removeParameter($name) + { + if (empty($name) || !is_string($name)) { + throw new Exception\InvalidArgumentException('Invalid parameter "name"' + . ' of "' . $name . '" must be a non-empty string'); + } + if (array_key_exists($name, $this->parameters)) { + unset($this->parameters[$name]); + } + return $this; + } + + /** + * Return an array of optional parameters for notification requests + * + * @return array + */ + public function getParameters() + { + return $this->parameters; + } + + /** + * Returns a boolean indicator of whether the notifications to Hub + * Servers were ALL successful. If even one failed, FALSE is returned. + * + * @return bool + */ + public function isSuccess() + { + return !(count($this->errors) != 0); + } + + /** + * Return an array of errors met from any failures, including keys: + * 'response' => the Zend\Http\Response object from the failure + * 'hubUrl' => the URL of the Hub Server whose notification failed + * + * @return array + */ + public function getErrors() + { + return $this->errors; + } + + /** + * Get a basic prepared HTTP client for use + * + * @return \Zend\Http\Client + * @throws Exception\RuntimeException + */ + protected function _getHttpClient() + { + $client = PubSubHubbub::getHttpClient(); + $client->setMethod(HttpRequest::METHOD_POST); + $client->setOptions(array( + 'useragent' => 'Zend_Feed_Pubsubhubbub_Publisher/' . Version::VERSION, + )); + $params = array(); + $params[] = 'hub.mode=publish'; + $topics = $this->getUpdatedTopicUrls(); + if (empty($topics)) { + throw new Exception\RuntimeException('No updated topic URLs' + . ' have been set'); + } + foreach ($topics as $topicUrl) { + $params[] = 'hub.url=' . urlencode($topicUrl); + } + $optParams = $this->getParameters(); + foreach ($optParams as $name => $value) { + $params[] = urlencode($name) . '=' . urlencode($value); + } + $paramString = implode('&', $params); + $client->setRawBody($paramString); + return $client; + } +} diff --git a/library/Zend/Feed/PubSubHubbub/Subscriber.php b/library/Zend/Feed/PubSubHubbub/Subscriber.php new file mode 100755 index 0000000000..265fe776b1 --- /dev/null +++ b/library/Zend/Feed/PubSubHubbub/Subscriber.php @@ -0,0 +1,837 @@ +setOptions($options); + } + } + + /** + * Process any injected configuration options + * + * @param array|Traversable $options + * @return Subscriber + * @throws Exception\InvalidArgumentException + */ + public function setOptions($options) + { + if ($options instanceof Traversable) { + $options = ArrayUtils::iteratorToArray($options); + } + + if (!is_array($options)) { + throw new Exception\InvalidArgumentException('Array or Traversable object' + . 'expected, got ' . gettype($options)); + } + if (array_key_exists('hubUrls', $options)) { + $this->addHubUrls($options['hubUrls']); + } + if (array_key_exists('callbackUrl', $options)) { + $this->setCallbackUrl($options['callbackUrl']); + } + if (array_key_exists('topicUrl', $options)) { + $this->setTopicUrl($options['topicUrl']); + } + if (array_key_exists('storage', $options)) { + $this->setStorage($options['storage']); + } + if (array_key_exists('leaseSeconds', $options)) { + $this->setLeaseSeconds($options['leaseSeconds']); + } + if (array_key_exists('parameters', $options)) { + $this->setParameters($options['parameters']); + } + if (array_key_exists('authentications', $options)) { + $this->addAuthentications($options['authentications']); + } + if (array_key_exists('usePathParameter', $options)) { + $this->usePathParameter($options['usePathParameter']); + } + if (array_key_exists('preferredVerificationMode', $options)) { + $this->setPreferredVerificationMode( + $options['preferredVerificationMode'] + ); + } + return $this; + } + + /** + * Set the topic URL (RSS or Atom feed) to which the intended (un)subscribe + * event will relate + * + * @param string $url + * @return Subscriber + * @throws Exception\InvalidArgumentException + */ + public function setTopicUrl($url) + { + if (empty($url) || !is_string($url) || !Uri::factory($url)->isValid()) { + throw new Exception\InvalidArgumentException('Invalid parameter "url"' + .' of "' . $url . '" must be a non-empty string and a valid' + .' URL'); + } + $this->topicUrl = $url; + return $this; + } + + /** + * Set the topic URL (RSS or Atom feed) to which the intended (un)subscribe + * event will relate + * + * @return string + * @throws Exception\RuntimeException + */ + public function getTopicUrl() + { + if (empty($this->topicUrl)) { + throw new Exception\RuntimeException('A valid Topic (RSS or Atom' + . ' feed) URL MUST be set before attempting any operation'); + } + return $this->topicUrl; + } + + /** + * Set the number of seconds for which any subscription will remain valid + * + * @param int $seconds + * @return Subscriber + * @throws Exception\InvalidArgumentException + */ + public function setLeaseSeconds($seconds) + { + $seconds = intval($seconds); + if ($seconds <= 0) { + throw new Exception\InvalidArgumentException('Expected lease seconds' + . ' must be an integer greater than zero'); + } + $this->leaseSeconds = $seconds; + return $this; + } + + /** + * Get the number of lease seconds on subscriptions + * + * @return int + */ + public function getLeaseSeconds() + { + return $this->leaseSeconds; + } + + /** + * Set the callback URL to be used by Hub Servers when communicating with + * this Subscriber + * + * @param string $url + * @return Subscriber + * @throws Exception\InvalidArgumentException + */ + public function setCallbackUrl($url) + { + if (empty($url) || !is_string($url) || !Uri::factory($url)->isValid()) { + throw new Exception\InvalidArgumentException('Invalid parameter "url"' + . ' of "' . $url . '" must be a non-empty string and a valid' + . ' URL'); + } + $this->callbackUrl = $url; + return $this; + } + + /** + * Get the callback URL to be used by Hub Servers when communicating with + * this Subscriber + * + * @return string + * @throws Exception\RuntimeException + */ + public function getCallbackUrl() + { + if (empty($this->callbackUrl)) { + throw new Exception\RuntimeException('A valid Callback URL MUST be' + . ' set before attempting any operation'); + } + return $this->callbackUrl; + } + + /** + * Set preferred verification mode (sync or async). By default, this + * Subscriber prefers synchronous verification, but does support + * asynchronous if that's the Hub Server's utilised mode. + * + * Zend\Feed\Pubsubhubbub\Subscriber will always send both modes, whose + * order of occurrence in the parameter list determines this preference. + * + * @param string $mode Should be 'sync' or 'async' + * @return Subscriber + * @throws Exception\InvalidArgumentException + */ + public function setPreferredVerificationMode($mode) + { + if ($mode !== PubSubHubbub::VERIFICATION_MODE_SYNC + && $mode !== PubSubHubbub::VERIFICATION_MODE_ASYNC + ) { + throw new Exception\InvalidArgumentException('Invalid preferred' + . ' mode specified: "' . $mode . '" but should be one of' + . ' Zend\Feed\Pubsubhubbub::VERIFICATION_MODE_SYNC or' + . ' Zend\Feed\Pubsubhubbub::VERIFICATION_MODE_ASYNC'); + } + $this->preferredVerificationMode = $mode; + return $this; + } + + /** + * Get preferred verification mode (sync or async). + * + * @return string + */ + public function getPreferredVerificationMode() + { + return $this->preferredVerificationMode; + } + + /** + * Add a Hub Server URL supported by Publisher + * + * @param string $url + * @return Subscriber + * @throws Exception\InvalidArgumentException + */ + public function addHubUrl($url) + { + if (empty($url) || !is_string($url) || !Uri::factory($url)->isValid()) { + throw new Exception\InvalidArgumentException('Invalid parameter "url"' + . ' of "' . $url . '" must be a non-empty string and a valid' + . ' URL'); + } + $this->hubUrls[] = $url; + return $this; + } + + /** + * Add an array of Hub Server URLs supported by Publisher + * + * @param array $urls + * @return Subscriber + */ + public function addHubUrls(array $urls) + { + foreach ($urls as $url) { + $this->addHubUrl($url); + } + return $this; + } + + /** + * Remove a Hub Server URL + * + * @param string $url + * @return Subscriber + */ + public function removeHubUrl($url) + { + if (!in_array($url, $this->getHubUrls())) { + return $this; + } + $key = array_search($url, $this->hubUrls); + unset($this->hubUrls[$key]); + return $this; + } + + /** + * Return an array of unique Hub Server URLs currently available + * + * @return array + */ + public function getHubUrls() + { + $this->hubUrls = array_unique($this->hubUrls); + return $this->hubUrls; + } + + /** + * Add authentication credentials for a given URL + * + * @param string $url + * @param array $authentication + * @return Subscriber + * @throws Exception\InvalidArgumentException + */ + public function addAuthentication($url, array $authentication) + { + if (empty($url) || !is_string($url) || !Uri::factory($url)->isValid()) { + throw new Exception\InvalidArgumentException('Invalid parameter "url"' + . ' of "' . $url . '" must be a non-empty string and a valid' + . ' URL'); + } + $this->authentications[$url] = $authentication; + return $this; + } + + /** + * Add authentication credentials for hub URLs + * + * @param array $authentications + * @return Subscriber + */ + public function addAuthentications(array $authentications) + { + foreach ($authentications as $url => $authentication) { + $this->addAuthentication($url, $authentication); + } + return $this; + } + + /** + * Get all hub URL authentication credentials + * + * @return array + */ + public function getAuthentications() + { + return $this->authentications; + } + + /** + * Set flag indicating whether or not to use a path parameter + * + * @param bool $bool + * @return Subscriber + */ + public function usePathParameter($bool = true) + { + $this->usePathParameter = $bool; + return $this; + } + + /** + * Add an optional parameter to the (un)subscribe requests + * + * @param string $name + * @param string|null $value + * @return Subscriber + * @throws Exception\InvalidArgumentException + */ + public function setParameter($name, $value = null) + { + if (is_array($name)) { + $this->setParameters($name); + return $this; + } + if (empty($name) || !is_string($name)) { + throw new Exception\InvalidArgumentException('Invalid parameter "name"' + . ' of "' . $name . '" must be a non-empty string'); + } + if ($value === null) { + $this->removeParameter($name); + return $this; + } + if (empty($value) || (!is_string($value) && $value !== null)) { + throw new Exception\InvalidArgumentException('Invalid parameter "value"' + . ' of "' . $value . '" must be a non-empty string'); + } + $this->parameters[$name] = $value; + return $this; + } + + /** + * Add an optional parameter to the (un)subscribe requests + * + * @param array $parameters + * @return Subscriber + */ + public function setParameters(array $parameters) + { + foreach ($parameters as $name => $value) { + $this->setParameter($name, $value); + } + return $this; + } + + /** + * Remove an optional parameter for the (un)subscribe requests + * + * @param string $name + * @return Subscriber + * @throws Exception\InvalidArgumentException + */ + public function removeParameter($name) + { + if (empty($name) || !is_string($name)) { + throw new Exception\InvalidArgumentException('Invalid parameter "name"' + . ' of "' . $name . '" must be a non-empty string'); + } + if (array_key_exists($name, $this->parameters)) { + unset($this->parameters[$name]); + } + return $this; + } + + /** + * Return an array of optional parameters for (un)subscribe requests + * + * @return array + */ + public function getParameters() + { + return $this->parameters; + } + + /** + * Sets an instance of Zend\Feed\Pubsubhubbub\Model\SubscriptionPersistence used to background + * save any verification tokens associated with a subscription or other. + * + * @param Model\SubscriptionPersistenceInterface $storage + * @return Subscriber + */ + public function setStorage(Model\SubscriptionPersistenceInterface $storage) + { + $this->storage = $storage; + return $this; + } + + /** + * Gets an instance of Zend\Feed\Pubsubhubbub\Storage\StoragePersistence used + * to background save any verification tokens associated with a subscription + * or other. + * + * @return Model\SubscriptionPersistenceInterface + * @throws Exception\RuntimeException + */ + public function getStorage() + { + if ($this->storage === null) { + throw new Exception\RuntimeException('No storage vehicle ' + . 'has been set.'); + } + return $this->storage; + } + + /** + * Subscribe to one or more Hub Servers using the stored Hub URLs + * for the given Topic URL (RSS or Atom feed) + * + * @return void + */ + public function subscribeAll() + { + $this->_doRequest('subscribe'); + } + + /** + * Unsubscribe from one or more Hub Servers using the stored Hub URLs + * for the given Topic URL (RSS or Atom feed) + * + * @return void + */ + public function unsubscribeAll() + { + $this->_doRequest('unsubscribe'); + } + + /** + * Returns a boolean indicator of whether the notifications to Hub + * Servers were ALL successful. If even one failed, FALSE is returned. + * + * @return bool + */ + public function isSuccess() + { + if (count($this->errors) > 0) { + return false; + } + return true; + } + + /** + * Return an array of errors met from any failures, including keys: + * 'response' => the Zend\Http\Response object from the failure + * 'hubUrl' => the URL of the Hub Server whose notification failed + * + * @return array + */ + public function getErrors() + { + return $this->errors; + } + + /** + * Return an array of Hub Server URLs who returned a response indicating + * operation in Asynchronous Verification Mode, i.e. they will not confirm + * any (un)subscription immediately but at a later time (Hubs may be + * doing this as a batch process when load balancing) + * + * @return array + */ + public function getAsyncHubs() + { + return $this->asyncHubs; + } + + /** + * Executes an (un)subscribe request + * + * @param string $mode + * @return void + * @throws Exception\RuntimeException + */ + protected function _doRequest($mode) + { + $client = $this->_getHttpClient(); + $hubs = $this->getHubUrls(); + if (empty($hubs)) { + throw new Exception\RuntimeException('No Hub Server URLs' + . ' have been set so no subscriptions can be attempted'); + } + $this->errors = array(); + $this->asyncHubs = array(); + foreach ($hubs as $url) { + if (array_key_exists($url, $this->authentications)) { + $auth = $this->authentications[$url]; + $client->setAuth($auth[0], $auth[1]); + } + $client->setUri($url); + $client->setRawBody($params = $this->_getRequestParameters($url, $mode)); + $response = $client->send(); + if ($response->getStatusCode() !== 204 + && $response->getStatusCode() !== 202 + ) { + $this->errors[] = array( + 'response' => $response, + 'hubUrl' => $url, + ); + /** + * At first I thought it was needed, but the backend storage will + * allow tracking async without any user interference. It's left + * here in case the user is interested in knowing what Hubs + * are using async verification modes so they may update Models and + * move these to asynchronous processes. + */ + } elseif ($response->getStatusCode() == 202) { + $this->asyncHubs[] = array( + 'response' => $response, + 'hubUrl' => $url, + ); + } + } + } + + /** + * Get a basic prepared HTTP client for use + * + * @return \Zend\Http\Client + */ + protected function _getHttpClient() + { + $client = PubSubHubbub::getHttpClient(); + $client->setMethod(HttpRequest::METHOD_POST); + $client->setOptions(array('useragent' => 'Zend_Feed_Pubsubhubbub_Subscriber/' + . Version::VERSION)); + return $client; + } + + /** + * Return a list of standard protocol/optional parameters for addition to + * client's POST body that are specific to the current Hub Server URL + * + * @param string $hubUrl + * @param string $mode + * @return string + * @throws Exception\InvalidArgumentException + */ + protected function _getRequestParameters($hubUrl, $mode) + { + if (!in_array($mode, array('subscribe', 'unsubscribe'))) { + throw new Exception\InvalidArgumentException('Invalid mode specified: "' + . $mode . '" which should have been "subscribe" or "unsubscribe"'); + } + + $params = array( + 'hub.mode' => $mode, + 'hub.topic' => $this->getTopicUrl(), + ); + + if ($this->getPreferredVerificationMode() + == PubSubHubbub::VERIFICATION_MODE_SYNC + ) { + $vmodes = array( + PubSubHubbub::VERIFICATION_MODE_SYNC, + PubSubHubbub::VERIFICATION_MODE_ASYNC, + ); + } else { + $vmodes = array( + PubSubHubbub::VERIFICATION_MODE_ASYNC, + PubSubHubbub::VERIFICATION_MODE_SYNC, + ); + } + $params['hub.verify'] = array(); + foreach ($vmodes as $vmode) { + $params['hub.verify'][] = $vmode; + } + + /** + * Establish a persistent verify_token and attach key to callback + * URL's path/query_string + */ + $key = $this->_generateSubscriptionKey($params, $hubUrl); + $token = $this->_generateVerifyToken(); + $params['hub.verify_token'] = $token; + + // Note: query string only usable with PuSH 0.2 Hubs + if (!$this->usePathParameter) { + $params['hub.callback'] = $this->getCallbackUrl() + . '?xhub.subscription=' . PubSubHubbub::urlencode($key); + } else { + $params['hub.callback'] = rtrim($this->getCallbackUrl(), '/') + . '/' . PubSubHubbub::urlencode($key); + } + if ($mode == 'subscribe' && $this->getLeaseSeconds() !== null) { + $params['hub.lease_seconds'] = $this->getLeaseSeconds(); + } + + // hub.secret not currently supported + $optParams = $this->getParameters(); + foreach ($optParams as $name => $value) { + $params[$name] = $value; + } + + // store subscription to storage + $now = new DateTime(); + $expires = null; + if (isset($params['hub.lease_seconds'])) { + $expires = $now->add(new DateInterval('PT' . $params['hub.lease_seconds'] . 'S')) + ->format('Y-m-d H:i:s'); + } + $data = array( + 'id' => $key, + 'topic_url' => $params['hub.topic'], + 'hub_url' => $hubUrl, + 'created_time' => $now->format('Y-m-d H:i:s'), + 'lease_seconds' => $params['hub.lease_seconds'], + 'verify_token' => hash('sha256', $params['hub.verify_token']), + 'secret' => null, + 'expiration_time' => $expires, + 'subscription_state' => ($mode == 'unsubscribe')? PubSubHubbub::SUBSCRIPTION_TODELETE : PubSubHubbub::SUBSCRIPTION_NOTVERIFIED, + ); + $this->getStorage()->setSubscription($data); + + return $this->_toByteValueOrderedString( + $this->_urlEncode($params) + ); + } + + /** + * Simple helper to generate a verification token used in (un)subscribe + * requests to a Hub Server. Follows no particular method, which means + * it might be improved/changed in future. + * + * @return string + */ + protected function _generateVerifyToken() + { + if (!empty($this->testStaticToken)) { + return $this->testStaticToken; + } + return uniqid(rand(), true) . time(); + } + + /** + * Simple helper to generate a verification token used in (un)subscribe + * requests to a Hub Server. + * + * @param array $params + * @param string $hubUrl The Hub Server URL for which this token will apply + * @return string + */ + protected function _generateSubscriptionKey(array $params, $hubUrl) + { + $keyBase = $params['hub.topic'] . $hubUrl; + $key = md5($keyBase); + + return $key; + } + + /** + * URL Encode an array of parameters + * + * @param array $params + * @return array + */ + protected function _urlEncode(array $params) + { + $encoded = array(); + foreach ($params as $key => $value) { + if (is_array($value)) { + $ekey = PubSubHubbub::urlencode($key); + $encoded[$ekey] = array(); + foreach ($value as $duplicateKey) { + $encoded[$ekey][] + = PubSubHubbub::urlencode($duplicateKey); + } + } else { + $encoded[PubSubHubbub::urlencode($key)] + = PubSubHubbub::urlencode($value); + } + } + return $encoded; + } + + /** + * Order outgoing parameters + * + * @param array $params + * @return array + */ + protected function _toByteValueOrderedString(array $params) + { + $return = array(); + uksort($params, 'strnatcmp'); + foreach ($params as $key => $value) { + if (is_array($value)) { + foreach ($value as $keyduplicate) { + $return[] = $key . '=' . $keyduplicate; + } + } else { + $return[] = $key . '=' . $value; + } + } + return implode('&', $return); + } + + /** + * This is STRICTLY for testing purposes only... + */ + protected $testStaticToken = null; + + final public function setTestStaticToken($token) + { + $this->testStaticToken = (string) $token; + } +} diff --git a/library/Zend/Feed/PubSubHubbub/Subscriber/Callback.php b/library/Zend/Feed/PubSubHubbub/Subscriber/Callback.php new file mode 100755 index 0000000000..5ec8af2fe1 --- /dev/null +++ b/library/Zend/Feed/PubSubHubbub/Subscriber/Callback.php @@ -0,0 +1,316 @@ +subscriptionKey = $key; + return $this; + } + + /** + * Handle any callback from a Hub Server responding to a subscription or + * unsubscription request. This should be the Hub Server confirming the + * the request prior to taking action on it. + * + * @param array $httpGetData GET data if available and not in $_GET + * @param bool $sendResponseNow Whether to send response now or when asked + * @return void + */ + public function handle(array $httpGetData = null, $sendResponseNow = false) + { + if ($httpGetData === null) { + $httpGetData = $_GET; + } + + /** + * Handle any feed updates (sorry for the mess :P) + * + * This DOES NOT attempt to process a feed update. Feed updates + * SHOULD be validated/processed by an asynchronous process so as + * to avoid holding up responses to the Hub. + */ + $contentType = $this->_getHeader('Content-Type'); + if (strtolower($_SERVER['REQUEST_METHOD']) == 'post' + && $this->_hasValidVerifyToken(null, false) + && (stripos($contentType, 'application/atom+xml') === 0 + || stripos($contentType, 'application/rss+xml') === 0 + || stripos($contentType, 'application/xml') === 0 + || stripos($contentType, 'text/xml') === 0 + || stripos($contentType, 'application/rdf+xml') === 0) + ) { + $this->setFeedUpdate($this->_getRawBody()); + $this->getHttpResponse()->setHeader('X-Hub-On-Behalf-Of', $this->getSubscriberCount()); + /** + * Handle any (un)subscribe confirmation requests + */ + } elseif ($this->isValidHubVerification($httpGetData)) { + $this->getHttpResponse()->setContent($httpGetData['hub_challenge']); + + switch (strtolower($httpGetData['hub_mode'])) { + case 'subscribe': + $data = $this->currentSubscriptionData; + $data['subscription_state'] = PubSubHubbub\PubSubHubbub::SUBSCRIPTION_VERIFIED; + if (isset($httpGetData['hub_lease_seconds'])) { + $data['lease_seconds'] = $httpGetData['hub_lease_seconds']; + } + $this->getStorage()->setSubscription($data); + break; + case 'unsubscribe': + $verifyTokenKey = $this->_detectVerifyTokenKey($httpGetData); + $this->getStorage()->deleteSubscription($verifyTokenKey); + break; + default: + throw new Exception\RuntimeException(sprintf( + 'Invalid hub_mode ("%s") provided', + $httpGetData['hub_mode'] + )); + } + /** + * Hey, C'mon! We tried everything else! + */ + } else { + $this->getHttpResponse()->setStatusCode(404); + } + + if ($sendResponseNow) { + $this->sendResponse(); + } + } + + /** + * Checks validity of the request simply by making a quick pass and + * confirming the presence of all REQUIRED parameters. + * + * @param array $httpGetData + * @return bool + */ + public function isValidHubVerification(array $httpGetData) + { + /** + * As per the specification, the hub.verify_token is OPTIONAL. This + * implementation of Pubsubhubbub considers it REQUIRED and will + * always send a hub.verify_token parameter to be echoed back + * by the Hub Server. Therefore, its absence is considered invalid. + */ + if (strtolower($_SERVER['REQUEST_METHOD']) !== 'get') { + return false; + } + $required = array( + 'hub_mode', + 'hub_topic', + 'hub_challenge', + 'hub_verify_token', + ); + foreach ($required as $key) { + if (!array_key_exists($key, $httpGetData)) { + return false; + } + } + if ($httpGetData['hub_mode'] !== 'subscribe' + && $httpGetData['hub_mode'] !== 'unsubscribe' + ) { + return false; + } + if ($httpGetData['hub_mode'] == 'subscribe' + && !array_key_exists('hub_lease_seconds', $httpGetData) + ) { + return false; + } + if (!Uri::factory($httpGetData['hub_topic'])->isValid()) { + return false; + } + + /** + * Attempt to retrieve any Verification Token Key attached to Callback + * URL's path by our Subscriber implementation + */ + if (!$this->_hasValidVerifyToken($httpGetData)) { + return false; + } + return true; + } + + /** + * Sets a newly received feed (Atom/RSS) sent by a Hub as an update to a + * Topic we've subscribed to. + * + * @param string $feed + * @return \Zend\Feed\PubSubHubbub\Subscriber\Callback + */ + public function setFeedUpdate($feed) + { + $this->feedUpdate = $feed; + return $this; + } + + /** + * Check if any newly received feed (Atom/RSS) update was received + * + * @return bool + */ + public function hasFeedUpdate() + { + if ($this->feedUpdate === null) { + return false; + } + return true; + } + + /** + * Gets a newly received feed (Atom/RSS) sent by a Hub as an update to a + * Topic we've subscribed to. + * + * @return string + */ + public function getFeedUpdate() + { + return $this->feedUpdate; + } + + /** + * Check for a valid verify_token. By default attempts to compare values + * with that sent from Hub, otherwise merely ascertains its existence. + * + * @param array $httpGetData + * @param bool $checkValue + * @return bool + */ + protected function _hasValidVerifyToken(array $httpGetData = null, $checkValue = true) + { + $verifyTokenKey = $this->_detectVerifyTokenKey($httpGetData); + if (empty($verifyTokenKey)) { + return false; + } + $verifyTokenExists = $this->getStorage()->hasSubscription($verifyTokenKey); + if (!$verifyTokenExists) { + return false; + } + if ($checkValue) { + $data = $this->getStorage()->getSubscription($verifyTokenKey); + $verifyToken = $data['verify_token']; + if ($verifyToken !== hash('sha256', $httpGetData['hub_verify_token'])) { + return false; + } + $this->currentSubscriptionData = $data; + return true; + } + return true; + } + + /** + * Attempt to detect the verification token key. This would be passed in + * the Callback URL (which we are handling with this class!) as a URI + * path part (the last part by convention). + * + * @param null|array $httpGetData + * @return false|string + */ + protected function _detectVerifyTokenKey(array $httpGetData = null) + { + /** + * Available when sub keys encoding in Callback URL path + */ + if (isset($this->subscriptionKey)) { + return $this->subscriptionKey; + } + + /** + * Available only if allowed by PuSH 0.2 Hubs + */ + if (is_array($httpGetData) + && isset($httpGetData['xhub_subscription']) + ) { + return $httpGetData['xhub_subscription']; + } + + /** + * Available (possibly) if corrupted in transit and not part of $_GET + */ + $params = $this->_parseQueryString(); + if (isset($params['xhub.subscription'])) { + return rawurldecode($params['xhub.subscription']); + } + + return false; + } + + /** + * Build an array of Query String parameters. + * This bypasses $_GET which munges parameter names and cannot accept + * multiple parameters with the same key. + * + * @return array|void + */ + protected function _parseQueryString() + { + $params = array(); + $queryString = ''; + if (isset($_SERVER['QUERY_STRING'])) { + $queryString = $_SERVER['QUERY_STRING']; + } + if (empty($queryString)) { + return array(); + } + $parts = explode('&', $queryString); + foreach ($parts as $kvpair) { + $pair = explode('=', $kvpair); + $key = rawurldecode($pair[0]); + $value = rawurldecode($pair[1]); + if (isset($params[$key])) { + if (is_array($params[$key])) { + $params[$key][] = $value; + } else { + $params[$key] = array($params[$key], $value); + } + } else { + $params[$key] = $value; + } + } + return $params; + } +} diff --git a/library/Zend/Feed/PubSubHubbub/Version.php b/library/Zend/Feed/PubSubHubbub/Version.php new file mode 100755 index 0000000000..edee6953bc --- /dev/null +++ b/library/Zend/Feed/PubSubHubbub/Version.php @@ -0,0 +1,15 @@ +entry = $entry; + $this->entryKey = $entryKey; + $this->domDocument = $entry->ownerDocument; + if ($type !== null) { + $this->data['type'] = $type; + } else { + $this->data['type'] = Reader::detectType($entry); + } + $this->_loadExtensions(); + } + + /** + * Get the DOM + * + * @return DOMDocument + */ + public function getDomDocument() + { + return $this->domDocument; + } + + /** + * Get the entry element + * + * @return DOMElement + */ + public function getElement() + { + return $this->entry; + } + + /** + * Get the Entry's encoding + * + * @return string + */ + public function getEncoding() + { + $assumed = $this->getDomDocument()->encoding; + if (empty($assumed)) { + $assumed = 'UTF-8'; + } + return $assumed; + } + + /** + * Get entry as xml + * + * @return string + */ + public function saveXml() + { + $dom = new DOMDocument('1.0', $this->getEncoding()); + $entry = $dom->importNode($this->getElement(), true); + $dom->appendChild($entry); + return $dom->saveXml(); + } + + /** + * Get the entry type + * + * @return string + */ + public function getType() + { + return $this->data['type']; + } + + /** + * Get the XPath query object + * + * @return DOMXPath + */ + public function getXpath() + { + if (!$this->xpath) { + $this->setXpath(new DOMXPath($this->getDomDocument())); + } + return $this->xpath; + } + + /** + * Set the XPath query + * + * @param DOMXPath $xpath + * @return \Zend\Feed\Reader\AbstractEntry + */ + public function setXpath(DOMXPath $xpath) + { + $this->xpath = $xpath; + return $this; + } + + /** + * Get registered extensions + * + * @return array + */ + public function getExtensions() + { + return $this->extensions; + } + + /** + * Return an Extension object with the matching name (postfixed with _Entry) + * + * @param string $name + * @return \Zend\Feed\Reader\Extension\AbstractEntry + */ + public function getExtension($name) + { + if (array_key_exists($name . '\Entry', $this->extensions)) { + return $this->extensions[$name . '\Entry']; + } + return null; + } + + /** + * Method overloading: call given method on first extension implementing it + * + * @param string $method + * @param array $args + * @return mixed + * @throws Exception\BadMethodCallException if no extensions implements the method + */ + public function __call($method, $args) + { + foreach ($this->extensions as $extension) { + if (method_exists($extension, $method)) { + return call_user_func_array(array($extension, $method), $args); + } + } + throw new Exception\BadMethodCallException('Method: ' . $method + . 'does not exist and could not be located on a registered Extension'); + } + + /** + * Load extensions from Zend\Feed\Reader\Reader + * + * @return void + */ + protected function _loadExtensions() + { + $all = Reader::getExtensions(); + $feed = $all['entry']; + foreach ($feed as $extension) { + if (in_array($extension, $all['core'])) { + continue; + } + $className = Reader::getPluginLoader()->getClassName($extension); + $this->extensions[$extension] = new $className( + $this->getElement(), $this->entryKey, $this->data['type'] + ); + } + } +} diff --git a/library/Zend/Feed/Reader/AbstractFeed.php b/library/Zend/Feed/Reader/AbstractFeed.php new file mode 100755 index 0000000000..f8aa49d81e --- /dev/null +++ b/library/Zend/Feed/Reader/AbstractFeed.php @@ -0,0 +1,300 @@ +domDocument = $domDocument; + $this->xpath = new DOMXPath($this->domDocument); + + if ($type !== null) { + $this->data['type'] = $type; + } else { + $this->data['type'] = Reader::detectType($this->domDocument); + } + $this->registerNamespaces(); + $this->indexEntries(); + $this->loadExtensions(); + } + + /** + * Set an original source URI for the feed being parsed. This value + * is returned from getFeedLink() method if the feed does not carry + * a self-referencing URI. + * + * @param string $uri + */ + public function setOriginalSourceUri($uri) + { + $this->originalSourceUri = $uri; + } + + /** + * Get an original source URI for the feed being parsed. Returns null if + * unset or the feed was not imported from a URI. + * + * @return string|null + */ + public function getOriginalSourceUri() + { + return $this->originalSourceUri; + } + + /** + * Get the number of feed entries. + * Required by the Iterator interface. + * + * @return int + */ + public function count() + { + return count($this->entries); + } + + /** + * Return the current entry + * + * @return \Zend\Feed\Reader\AbstractEntry + */ + public function current() + { + if (substr($this->getType(), 0, 3) == 'rss') { + $reader = new Entry\RSS($this->entries[$this->key()], $this->key(), $this->getType()); + } else { + $reader = new Entry\Atom($this->entries[$this->key()], $this->key(), $this->getType()); + } + + $reader->setXpath($this->xpath); + + return $reader; + } + + /** + * Get the DOM + * + * @return DOMDocument + */ + public function getDomDocument() + { + return $this->domDocument; + } + + /** + * Get the Feed's encoding + * + * @return string + */ + public function getEncoding() + { + $assumed = $this->getDomDocument()->encoding; + if (empty($assumed)) { + $assumed = 'UTF-8'; + } + return $assumed; + } + + /** + * Get feed as xml + * + * @return string + */ + public function saveXml() + { + return $this->getDomDocument()->saveXml(); + } + + /** + * Get the DOMElement representing the items/feed element + * + * @return DOMElement + */ + public function getElement() + { + return $this->getDomDocument()->documentElement; + } + + /** + * Get the DOMXPath object for this feed + * + * @return DOMXPath + */ + public function getXpath() + { + return $this->xpath; + } + + /** + * Get the feed type + * + * @return string + */ + public function getType() + { + return $this->data['type']; + } + + /** + * Return the current feed key + * + * @return int + */ + public function key() + { + return $this->entriesKey; + } + + /** + * Move the feed pointer forward + * + */ + public function next() + { + ++$this->entriesKey; + } + + /** + * Reset the pointer in the feed object + * + */ + public function rewind() + { + $this->entriesKey = 0; + } + + /** + * Check to see if the iterator is still valid + * + * @return bool + */ + public function valid() + { + return 0 <= $this->entriesKey && $this->entriesKey < $this->count(); + } + + public function getExtensions() + { + return $this->extensions; + } + + public function __call($method, $args) + { + foreach ($this->extensions as $extension) { + if (method_exists($extension, $method)) { + return call_user_func_array(array($extension, $method), $args); + } + } + throw new Exception\BadMethodCallException('Method: ' . $method + . 'does not exist and could not be located on a registered Extension'); + } + + /** + * Return an Extension object with the matching name (postfixed with _Feed) + * + * @param string $name + * @return \Zend\Feed\Reader\Extension\AbstractFeed + */ + public function getExtension($name) + { + if (array_key_exists($name . '\Feed', $this->extensions)) { + return $this->extensions[$name . '\Feed']; + } + return null; + } + + protected function loadExtensions() + { + $all = Reader::getExtensions(); + $manager = Reader::getExtensionManager(); + $feed = $all['feed']; + foreach ($feed as $extension) { + if (in_array($extension, $all['core'])) { + continue; + } + $plugin = $manager->get($extension); + $plugin->setDomDocument($this->getDomDocument()); + $plugin->setType($this->data['type']); + $plugin->setXpath($this->xpath); + $this->extensions[$extension] = $plugin; + } + } + + /** + * Read all entries to the internal entries array + * + */ + abstract protected function indexEntries(); + + /** + * Register the default namespaces for the current feed format + * + */ + abstract protected function registerNamespaces(); +} diff --git a/library/Zend/Feed/Reader/Collection.php b/library/Zend/Feed/Reader/Collection.php new file mode 100755 index 0000000000..ac1c96384a --- /dev/null +++ b/library/Zend/Feed/Reader/Collection.php @@ -0,0 +1,16 @@ +getIterator() as $element) { + $authors[] = $element['name']; + } + return array_unique($authors); + } +} diff --git a/library/Zend/Feed/Reader/Collection/Category.php b/library/Zend/Feed/Reader/Collection/Category.php new file mode 100755 index 0000000000..34b8fdedb4 --- /dev/null +++ b/library/Zend/Feed/Reader/Collection/Category.php @@ -0,0 +1,34 @@ +getIterator() as $element) { + if (isset($element['label']) && !empty($element['label'])) { + $categories[] = $element['label']; + } else { + $categories[] = $element['term']; + } + } + return array_unique($categories); + } +} diff --git a/library/Zend/Feed/Reader/Collection/Collection.php b/library/Zend/Feed/Reader/Collection/Collection.php new file mode 100755 index 0000000000..86a29276ac --- /dev/null +++ b/library/Zend/Feed/Reader/Collection/Collection.php @@ -0,0 +1,16 @@ +entry = $entry; + $this->entryKey = $entryKey; + $this->domDocument = $entry->ownerDocument; + if ($type !== null) { + $this->data['type'] = $type; + } elseif ($this->domDocument !== null) { + $this->data['type'] = Reader\Reader::detectType($this->domDocument); + } else { + $this->data['type'] = Reader\Reader::TYPE_ANY; + } + $this->loadExtensions(); + } + + /** + * Get the DOM + * + * @return DOMDocument + */ + public function getDomDocument() + { + return $this->domDocument; + } + + /** + * Get the entry element + * + * @return DOMElement + */ + public function getElement() + { + return $this->entry; + } + + /** + * Get the Entry's encoding + * + * @return string + */ + public function getEncoding() + { + $assumed = $this->getDomDocument()->encoding; + if (empty($assumed)) { + $assumed = 'UTF-8'; + } + return $assumed; + } + + /** + * Get entry as xml + * + * @return string + */ + public function saveXml() + { + $dom = new DOMDocument('1.0', $this->getEncoding()); + $entry = $dom->importNode($this->getElement(), true); + $dom->appendChild($entry); + return $dom->saveXml(); + } + + /** + * Get the entry type + * + * @return string + */ + public function getType() + { + return $this->data['type']; + } + + /** + * Get the XPath query object + * + * @return DOMXPath + */ + public function getXpath() + { + if (!$this->xpath) { + $this->setXpath(new DOMXPath($this->getDomDocument())); + } + return $this->xpath; + } + + /** + * Set the XPath query + * + * @param DOMXPath $xpath + * @return AbstractEntry + */ + public function setXpath(DOMXPath $xpath) + { + $this->xpath = $xpath; + return $this; + } + + /** + * Get registered extensions + * + * @return array + */ + public function getExtensions() + { + return $this->extensions; + } + + /** + * Return an Extension object with the matching name (postfixed with _Entry) + * + * @param string $name + * @return Reader\Extension\AbstractEntry + */ + public function getExtension($name) + { + if (array_key_exists($name . '\\Entry', $this->extensions)) { + return $this->extensions[$name . '\\Entry']; + } + return null; + } + + /** + * Method overloading: call given method on first extension implementing it + * + * @param string $method + * @param array $args + * @return mixed + * @throws Exception\RuntimeException if no extensions implements the method + */ + public function __call($method, $args) + { + foreach ($this->extensions as $extension) { + if (method_exists($extension, $method)) { + return call_user_func_array(array($extension, $method), $args); + } + } + throw new Exception\RuntimeException('Method: ' . $method + . ' does not exist and could not be located on a registered Extension'); + } + + /** + * Load extensions from Zend\Feed\Reader\Reader + * + * @return void + */ + protected function loadExtensions() + { + $all = Reader\Reader::getExtensions(); + $manager = Reader\Reader::getExtensionManager(); + $feed = $all['entry']; + foreach ($feed as $extension) { + if (in_array($extension, $all['core'])) { + continue; + } + $plugin = $manager->get($extension); + $plugin->setEntryElement($this->getElement()); + $plugin->setEntryKey($this->entryKey); + $plugin->setType($this->data['type']); + $this->extensions[$extension] = $plugin; + } + } +} diff --git a/library/Zend/Feed/Reader/Entry/Atom.php b/library/Zend/Feed/Reader/Entry/Atom.php new file mode 100755 index 0000000000..ed61a21e5f --- /dev/null +++ b/library/Zend/Feed/Reader/Entry/Atom.php @@ -0,0 +1,370 @@ +xpathQuery = '//atom:entry[' . ($this->entryKey + 1) . ']'; + + $manager = Reader\Reader::getExtensionManager(); + $extensions = array('Atom\Entry', 'Thread\Entry', 'DublinCore\Entry'); + + foreach ($extensions as $name) { + $extension = $manager->get($name); + $extension->setEntryElement($entry); + $extension->setEntryKey($entryKey); + $extension->setType($type); + $this->extensions[$name] = $extension; + } + } + + /** + * Get the specified author + * + * @param int $index + * @return string|null + */ + public function getAuthor($index = 0) + { + $authors = $this->getAuthors(); + + if (isset($authors[$index])) { + return $authors[$index]; + } + + return null; + } + + /** + * Get an array with feed authors + * + * @return array + */ + public function getAuthors() + { + if (array_key_exists('authors', $this->data)) { + return $this->data['authors']; + } + + $people = $this->getExtension('Atom')->getAuthors(); + + $this->data['authors'] = $people; + + return $this->data['authors']; + } + + /** + * Get the entry content + * + * @return string + */ + public function getContent() + { + if (array_key_exists('content', $this->data)) { + return $this->data['content']; + } + + $content = $this->getExtension('Atom')->getContent(); + + $this->data['content'] = $content; + + return $this->data['content']; + } + + /** + * Get the entry creation date + * + * @return string + */ + public function getDateCreated() + { + if (array_key_exists('datecreated', $this->data)) { + return $this->data['datecreated']; + } + + $dateCreated = $this->getExtension('Atom')->getDateCreated(); + + $this->data['datecreated'] = $dateCreated; + + return $this->data['datecreated']; + } + + /** + * Get the entry modification date + * + * @return string + */ + public function getDateModified() + { + if (array_key_exists('datemodified', $this->data)) { + return $this->data['datemodified']; + } + + $dateModified = $this->getExtension('Atom')->getDateModified(); + + $this->data['datemodified'] = $dateModified; + + return $this->data['datemodified']; + } + + /** + * Get the entry description + * + * @return string + */ + public function getDescription() + { + if (array_key_exists('description', $this->data)) { + return $this->data['description']; + } + + $description = $this->getExtension('Atom')->getDescription(); + + $this->data['description'] = $description; + + return $this->data['description']; + } + + /** + * Get the entry enclosure + * + * @return string + */ + public function getEnclosure() + { + if (array_key_exists('enclosure', $this->data)) { + return $this->data['enclosure']; + } + + $enclosure = $this->getExtension('Atom')->getEnclosure(); + + $this->data['enclosure'] = $enclosure; + + return $this->data['enclosure']; + } + + /** + * Get the entry ID + * + * @return string + */ + public function getId() + { + if (array_key_exists('id', $this->data)) { + return $this->data['id']; + } + + $id = $this->getExtension('Atom')->getId(); + + $this->data['id'] = $id; + + return $this->data['id']; + } + + /** + * Get a specific link + * + * @param int $index + * @return string + */ + public function getLink($index = 0) + { + if (!array_key_exists('links', $this->data)) { + $this->getLinks(); + } + + if (isset($this->data['links'][$index])) { + return $this->data['links'][$index]; + } + + return null; + } + + /** + * Get all links + * + * @return array + */ + public function getLinks() + { + if (array_key_exists('links', $this->data)) { + return $this->data['links']; + } + + $links = $this->getExtension('Atom')->getLinks(); + + $this->data['links'] = $links; + + return $this->data['links']; + } + + /** + * Get a permalink to the entry + * + * @return string + */ + public function getPermalink() + { + return $this->getLink(0); + } + + /** + * Get the entry title + * + * @return string + */ + public function getTitle() + { + if (array_key_exists('title', $this->data)) { + return $this->data['title']; + } + + $title = $this->getExtension('Atom')->getTitle(); + + $this->data['title'] = $title; + + return $this->data['title']; + } + + /** + * Get the number of comments/replies for current entry + * + * @return int + */ + public function getCommentCount() + { + if (array_key_exists('commentcount', $this->data)) { + return $this->data['commentcount']; + } + + $commentcount = $this->getExtension('Thread')->getCommentCount(); + + if (!$commentcount) { + $commentcount = $this->getExtension('Atom')->getCommentCount(); + } + + $this->data['commentcount'] = $commentcount; + + return $this->data['commentcount']; + } + + /** + * Returns a URI pointing to the HTML page where comments can be made on this entry + * + * @return string + */ + public function getCommentLink() + { + if (array_key_exists('commentlink', $this->data)) { + return $this->data['commentlink']; + } + + $commentlink = $this->getExtension('Atom')->getCommentLink(); + + $this->data['commentlink'] = $commentlink; + + return $this->data['commentlink']; + } + + /** + * Returns a URI pointing to a feed of all comments for this entry + * + * @return string + */ + public function getCommentFeedLink() + { + if (array_key_exists('commentfeedlink', $this->data)) { + return $this->data['commentfeedlink']; + } + + $commentfeedlink = $this->getExtension('Atom')->getCommentFeedLink(); + + $this->data['commentfeedlink'] = $commentfeedlink; + + return $this->data['commentfeedlink']; + } + + /** + * Get category data as a Reader\Reader_Collection_Category object + * + * @return Reader\Collection\Category + */ + public function getCategories() + { + if (array_key_exists('categories', $this->data)) { + return $this->data['categories']; + } + + $categoryCollection = $this->getExtension('Atom')->getCategories(); + + if (count($categoryCollection) == 0) { + $categoryCollection = $this->getExtension('DublinCore')->getCategories(); + } + + $this->data['categories'] = $categoryCollection; + + return $this->data['categories']; + } + + /** + * Get source feed metadata from the entry + * + * @return Reader\Feed\Atom\Source|null + */ + public function getSource() + { + if (array_key_exists('source', $this->data)) { + return $this->data['source']; + } + + $source = $this->getExtension('Atom')->getSource(); + + $this->data['source'] = $source; + + return $this->data['source']; + } + + /** + * Set the XPath query (incl. on all Extensions) + * + * @param DOMXPath $xpath + * @return void + */ + public function setXpath(DOMXPath $xpath) + { + parent::setXpath($xpath); + foreach ($this->extensions as $extension) { + $extension->setXpath($this->xpath); + } + } +} diff --git a/library/Zend/Feed/Reader/Entry/EntryInterface.php b/library/Zend/Feed/Reader/Entry/EntryInterface.php new file mode 100755 index 0000000000..86fea3ec58 --- /dev/null +++ b/library/Zend/Feed/Reader/Entry/EntryInterface.php @@ -0,0 +1,129 @@ +xpathQueryRss = '//item[' . ($this->entryKey+1) . ']'; + $this->xpathQueryRdf = '//rss:item[' . ($this->entryKey+1) . ']'; + + $manager = Reader\Reader::getExtensionManager(); + $extensions = array( + 'DublinCore\Entry', + 'Content\Entry', + 'Atom\Entry', + 'WellFormedWeb\Entry', + 'Slash\Entry', + 'Thread\Entry', + ); + foreach ($extensions as $name) { + $extension = $manager->get($name); + $extension->setEntryElement($entry); + $extension->setEntryKey($entryKey); + $extension->setType($type); + $this->extensions[$name] = $extension; + } + } + + /** + * Get an author entry + * + * @param int $index + * @return string + */ + public function getAuthor($index = 0) + { + $authors = $this->getAuthors(); + + if (isset($authors[$index])) { + return $authors[$index]; + } + + return null; + } + + /** + * Get an array with feed authors + * + * @return array + */ + public function getAuthors() + { + if (array_key_exists('authors', $this->data)) { + return $this->data['authors']; + } + + $authors = array(); + $authorsDc = $this->getExtension('DublinCore')->getAuthors(); + if (!empty($authorsDc)) { + foreach ($authorsDc as $author) { + $authors[] = array( + 'name' => $author['name'] + ); + } + } + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 + && $this->getType() !== Reader\Reader::TYPE_RSS_090) { + $list = $this->xpath->query($this->xpathQueryRss . '//author'); + } else { + $list = $this->xpath->query($this->xpathQueryRdf . '//rss:author'); + } + if ($list->length) { + foreach ($list as $author) { + $string = trim($author->nodeValue); + $email = null; + $name = null; + $data = array(); + // Pretty rough parsing - but it's a catchall + if (preg_match("/^.*@[^ ]*/", $string, $matches)) { + $data['email'] = trim($matches[0]); + if (preg_match("/\((.*)\)$/", $string, $matches)) { + $data['name'] = $matches[1]; + } + $authors[] = $data; + } + } + } + + if (count($authors) == 0) { + $authors = $this->getExtension('Atom')->getAuthors(); + } else { + $authors = new Reader\Collection\Author( + Reader\Reader::arrayUnique($authors) + ); + } + + if (count($authors) == 0) { + $authors = null; + } + + $this->data['authors'] = $authors; + + return $this->data['authors']; + } + + /** + * Get the entry content + * + * @return string + */ + public function getContent() + { + if (array_key_exists('content', $this->data)) { + return $this->data['content']; + } + + $content = $this->getExtension('Content')->getContent(); + + if (!$content) { + $content = $this->getDescription(); + } + + if (empty($content)) { + $content = $this->getExtension('Atom')->getContent(); + } + + $this->data['content'] = $content; + + return $this->data['content']; + } + + /** + * Get the entry's date of creation + * + * @return string + */ + public function getDateCreated() + { + return $this->getDateModified(); + } + + /** + * Get the entry's date of modification + * + * @throws Exception\RuntimeException + * @return string + */ + public function getDateModified() + { + if (array_key_exists('datemodified', $this->data)) { + return $this->data['datemodified']; + } + + $dateModified = null; + $date = null; + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 + && $this->getType() !== Reader\Reader::TYPE_RSS_090 + ) { + $dateModified = $this->xpath->evaluate('string(' . $this->xpathQueryRss . '/pubDate)'); + if ($dateModified) { + $dateModifiedParsed = strtotime($dateModified); + if ($dateModifiedParsed) { + $date = new DateTime('@' . $dateModifiedParsed); + } else { + $dateStandards = array(DateTime::RSS, DateTime::RFC822, + DateTime::RFC2822, null); + foreach ($dateStandards as $standard) { + try { + $date = date_create_from_format($standard, $dateModified); + break; + } catch (\Exception $e) { + if ($standard == null) { + throw new Exception\RuntimeException( + 'Could not load date due to unrecognised' + .' format (should follow RFC 822 or 2822):' + . $e->getMessage(), + 0, $e + ); + } + } + } + } + } + } + + if (!$date) { + $date = $this->getExtension('DublinCore')->getDate(); + } + + if (!$date) { + $date = $this->getExtension('Atom')->getDateModified(); + } + + if (!$date) { + $date = null; + } + + $this->data['datemodified'] = $date; + + return $this->data['datemodified']; + } + + /** + * Get the entry description + * + * @return string + */ + public function getDescription() + { + if (array_key_exists('description', $this->data)) { + return $this->data['description']; + } + + $description = null; + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 + && $this->getType() !== Reader\Reader::TYPE_RSS_090 + ) { + $description = $this->xpath->evaluate('string(' . $this->xpathQueryRss . '/description)'); + } else { + $description = $this->xpath->evaluate('string(' . $this->xpathQueryRdf . '/rss:description)'); + } + + if (!$description) { + $description = $this->getExtension('DublinCore')->getDescription(); + } + + if (empty($description)) { + $description = $this->getExtension('Atom')->getDescription(); + } + + if (!$description) { + $description = null; + } + + $this->data['description'] = $description; + + return $this->data['description']; + } + + /** + * Get the entry enclosure + * @return string + */ + public function getEnclosure() + { + if (array_key_exists('enclosure', $this->data)) { + return $this->data['enclosure']; + } + + $enclosure = null; + + if ($this->getType() == Reader\Reader::TYPE_RSS_20) { + $nodeList = $this->xpath->query($this->xpathQueryRss . '/enclosure'); + + if ($nodeList->length > 0) { + $enclosure = new \stdClass(); + $enclosure->url = $nodeList->item(0)->getAttribute('url'); + $enclosure->length = $nodeList->item(0)->getAttribute('length'); + $enclosure->type = $nodeList->item(0)->getAttribute('type'); + } + } + + if (!$enclosure) { + $enclosure = $this->getExtension('Atom')->getEnclosure(); + } + + $this->data['enclosure'] = $enclosure; + + return $this->data['enclosure']; + } + + /** + * Get the entry ID + * + * @return string + */ + public function getId() + { + if (array_key_exists('id', $this->data)) { + return $this->data['id']; + } + + $id = null; + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 + && $this->getType() !== Reader\Reader::TYPE_RSS_090 + ) { + $id = $this->xpath->evaluate('string(' . $this->xpathQueryRss . '/guid)'); + } + + if (!$id) { + $id = $this->getExtension('DublinCore')->getId(); + } + + if (empty($id)) { + $id = $this->getExtension('Atom')->getId(); + } + + if (!$id) { + if ($this->getPermalink()) { + $id = $this->getPermalink(); + } elseif ($this->getTitle()) { + $id = $this->getTitle(); + } else { + $id = null; + } + } + + $this->data['id'] = $id; + + return $this->data['id']; + } + + /** + * Get a specific link + * + * @param int $index + * @return string + */ + public function getLink($index = 0) + { + if (!array_key_exists('links', $this->data)) { + $this->getLinks(); + } + + if (isset($this->data['links'][$index])) { + return $this->data['links'][$index]; + } + + return null; + } + + /** + * Get all links + * + * @return array + */ + public function getLinks() + { + if (array_key_exists('links', $this->data)) { + return $this->data['links']; + } + + $links = array(); + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 && + $this->getType() !== Reader\Reader::TYPE_RSS_090) { + $list = $this->xpath->query($this->xpathQueryRss . '//link'); + } else { + $list = $this->xpath->query($this->xpathQueryRdf . '//rss:link'); + } + + if (!$list->length) { + $links = $this->getExtension('Atom')->getLinks(); + } else { + foreach ($list as $link) { + $links[] = $link->nodeValue; + } + } + + $this->data['links'] = $links; + + return $this->data['links']; + } + + /** + * Get all categories + * + * @return Reader\Collection\Category + */ + public function getCategories() + { + if (array_key_exists('categories', $this->data)) { + return $this->data['categories']; + } + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 && + $this->getType() !== Reader\Reader::TYPE_RSS_090) { + $list = $this->xpath->query($this->xpathQueryRss . '//category'); + } else { + $list = $this->xpath->query($this->xpathQueryRdf . '//rss:category'); + } + + if ($list->length) { + $categoryCollection = new Reader\Collection\Category; + foreach ($list as $category) { + $categoryCollection[] = array( + 'term' => $category->nodeValue, + 'scheme' => $category->getAttribute('domain'), + 'label' => $category->nodeValue, + ); + } + } else { + $categoryCollection = $this->getExtension('DublinCore')->getCategories(); + } + + if (count($categoryCollection) == 0) { + $categoryCollection = $this->getExtension('Atom')->getCategories(); + } + + $this->data['categories'] = $categoryCollection; + + return $this->data['categories']; + } + + /** + * Get a permalink to the entry + * + * @return string + */ + public function getPermalink() + { + return $this->getLink(0); + } + + /** + * Get the entry title + * + * @return string + */ + public function getTitle() + { + if (array_key_exists('title', $this->data)) { + return $this->data['title']; + } + + $title = null; + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 + && $this->getType() !== Reader\Reader::TYPE_RSS_090 + ) { + $title = $this->xpath->evaluate('string(' . $this->xpathQueryRss . '/title)'); + } else { + $title = $this->xpath->evaluate('string(' . $this->xpathQueryRdf . '/rss:title)'); + } + + if (!$title) { + $title = $this->getExtension('DublinCore')->getTitle(); + } + + if (!$title) { + $title = $this->getExtension('Atom')->getTitle(); + } + + if (!$title) { + $title = null; + } + + $this->data['title'] = $title; + + return $this->data['title']; + } + + /** + * Get the number of comments/replies for current entry + * + * @return string|null + */ + public function getCommentCount() + { + if (array_key_exists('commentcount', $this->data)) { + return $this->data['commentcount']; + } + + $commentcount = $this->getExtension('Slash')->getCommentCount(); + + if (!$commentcount) { + $commentcount = $this->getExtension('Thread')->getCommentCount(); + } + + if (!$commentcount) { + $commentcount = $this->getExtension('Atom')->getCommentCount(); + } + + if (!$commentcount) { + $commentcount = null; + } + + $this->data['commentcount'] = $commentcount; + + return $this->data['commentcount']; + } + + /** + * Returns a URI pointing to the HTML page where comments can be made on this entry + * + * @return string + */ + public function getCommentLink() + { + if (array_key_exists('commentlink', $this->data)) { + return $this->data['commentlink']; + } + + $commentlink = null; + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 + && $this->getType() !== Reader\Reader::TYPE_RSS_090 + ) { + $commentlink = $this->xpath->evaluate('string(' . $this->xpathQueryRss . '/comments)'); + } + + if (!$commentlink) { + $commentlink = $this->getExtension('Atom')->getCommentLink(); + } + + if (!$commentlink) { + $commentlink = null; + } + + $this->data['commentlink'] = $commentlink; + + return $this->data['commentlink']; + } + + /** + * Returns a URI pointing to a feed of all comments for this entry + * + * @return string + */ + public function getCommentFeedLink() + { + if (array_key_exists('commentfeedlink', $this->data)) { + return $this->data['commentfeedlink']; + } + + $commentfeedlink = $this->getExtension('WellFormedWeb')->getCommentFeedLink(); + + if (!$commentfeedlink) { + $commentfeedlink = $this->getExtension('Atom')->getCommentFeedLink('rss'); + } + + if (!$commentfeedlink) { + $commentfeedlink = $this->getExtension('Atom')->getCommentFeedLink('rdf'); + } + + if (!$commentfeedlink) { + $commentfeedlink = null; + } + + $this->data['commentfeedlink'] = $commentfeedlink; + + return $this->data['commentfeedlink']; + } + + /** + * Set the XPath query (incl. on all Extensions) + * + * @param DOMXPath $xpath + * @return void + */ + public function setXpath(DOMXPath $xpath) + { + parent::setXpath($xpath); + foreach ($this->extensions as $extension) { + $extension->setXpath($this->xpath); + } + } +} diff --git a/library/Zend/Feed/Reader/Exception/BadMethodCallException.php b/library/Zend/Feed/Reader/Exception/BadMethodCallException.php new file mode 100755 index 0000000000..3e265088a5 --- /dev/null +++ b/library/Zend/Feed/Reader/Exception/BadMethodCallException.php @@ -0,0 +1,18 @@ +entry = $entry; + $this->domDocument = $entry->ownerDocument; + return $this; + } + + /** + * Get the entry DOMElement + * + * @return DOMElement + */ + public function getEntryElement() + { + return $this->entry; + } + + /** + * Set the entry key + * + * @param string $entryKey + * @return AbstractEntry + */ + public function setEntryKey($entryKey) + { + $this->entryKey = $entryKey; + return $this; + } + + /** + * Get the DOM + * + * @return DOMDocument + */ + public function getDomDocument() + { + return $this->domDocument; + } + + /** + * Get the Entry's encoding + * + * @return string + */ + public function getEncoding() + { + $assumed = $this->getDomDocument()->encoding; + return $assumed; + } + + /** + * Set the entry type + * + * Has side effect of setting xpath prefix + * + * @param string $type + * @return AbstractEntry + */ + public function setType($type) + { + if (null === $type) { + $this->data['type'] = null; + return $this; + } + + $this->data['type'] = $type; + if ($type === Reader\Reader::TYPE_RSS_10 + || $type === Reader\Reader::TYPE_RSS_090 + ) { + $this->setXpathPrefix('//rss:item[' . ($this->entryKey + 1) . ']'); + return $this; + } + + if ($type === Reader\Reader::TYPE_ATOM_10 + || $type === Reader\Reader::TYPE_ATOM_03 + ) { + $this->setXpathPrefix('//atom:entry[' . ($this->entryKey + 1) . ']'); + return $this; + } + + $this->setXpathPrefix('//item[' . ($this->entryKey + 1) . ']'); + return $this; + } + + /** + * Get the entry type + * + * @return string + */ + public function getType() + { + $type = $this->data['type']; + if ($type === null) { + $type = Reader\Reader::detectType($this->getEntryElement(), true); + $this->setType($type); + } + + return $type; + } + + /** + * Set the XPath query + * + * @param DOMXPath $xpath + * @return AbstractEntry + */ + public function setXpath(DOMXPath $xpath) + { + $this->xpath = $xpath; + $this->registerNamespaces(); + return $this; + } + + /** + * Get the XPath query object + * + * @return DOMXPath + */ + public function getXpath() + { + if (!$this->xpath) { + $this->setXpath(new DOMXPath($this->getDomDocument())); + } + return $this->xpath; + } + + /** + * Serialize the entry to an array + * + * @return array + */ + public function toArray() + { + return $this->data; + } + + /** + * Get the XPath prefix + * + * @return string + */ + public function getXpathPrefix() + { + return $this->xpathPrefix; + } + + /** + * Set the XPath prefix + * + * @param string $prefix + * @return AbstractEntry + */ + public function setXpathPrefix($prefix) + { + $this->xpathPrefix = $prefix; + return $this; + } + + /** + * Register XML namespaces + * + * @return void + */ + abstract protected function registerNamespaces(); +} diff --git a/library/Zend/Feed/Reader/Extension/AbstractFeed.php b/library/Zend/Feed/Reader/Extension/AbstractFeed.php new file mode 100755 index 0000000000..1bea2e4980 --- /dev/null +++ b/library/Zend/Feed/Reader/Extension/AbstractFeed.php @@ -0,0 +1,176 @@ +domDocument = $dom; + return $this; + } + + /** + * Get the DOM + * + * @return DOMDocument + */ + public function getDomDocument() + { + return $this->domDocument; + } + + /** + * Get the Feed's encoding + * + * @return string + */ + public function getEncoding() + { + $assumed = $this->getDomDocument()->encoding; + return $assumed; + } + + /** + * Set the feed type + * + * @param string $type + * @return AbstractFeed + */ + public function setType($type) + { + $this->data['type'] = $type; + return $this; + } + + /** + * Get the feed type + * + * If null, it will attempt to autodetect the type. + * + * @return string + */ + public function getType() + { + $type = $this->data['type']; + if (null === $type) { + $type = Reader\Reader::detectType($this->getDomDocument()); + $this->setType($type); + } + return $type; + } + + + /** + * Return the feed as an array + * + * @return array + */ + public function toArray() // untested + { + return $this->data; + } + + /** + * Set the XPath query + * + * @param DOMXPath $xpath + * @return AbstractEntry + */ + public function setXpath(DOMXPath $xpath = null) + { + if (null === $xpath) { + $this->xpath = null; + return $this; + } + + $this->xpath = $xpath; + $this->registerNamespaces(); + return $this; + } + + /** + * Get the DOMXPath object + * + * @return string + */ + public function getXpath() + { + if (null === $this->xpath) { + $this->setXpath(new DOMXPath($this->getDomDocument())); + } + + return $this->xpath; + } + + /** + * Get the XPath prefix + * + * @return string + */ + public function getXpathPrefix() + { + return $this->xpathPrefix; + } + + /** + * Set the XPath prefix + * + * @param string $prefix + * @return void + */ + public function setXpathPrefix($prefix) + { + $this->xpathPrefix = $prefix; + } + + /** + * Register the default namespaces for the current feed format + */ + abstract protected function registerNamespaces(); +} diff --git a/library/Zend/Feed/Reader/Extension/Atom/Entry.php b/library/Zend/Feed/Reader/Extension/Atom/Entry.php new file mode 100755 index 0000000000..9e20321cde --- /dev/null +++ b/library/Zend/Feed/Reader/Extension/Atom/Entry.php @@ -0,0 +1,630 @@ +getAuthors(); + + if (isset($authors[$index])) { + return $authors[$index]; + } + + return null; + } + + /** + * Get an array with feed authors + * + * @return Collection\Author + */ + public function getAuthors() + { + if (array_key_exists('authors', $this->data)) { + return $this->data['authors']; + } + + $authors = array(); + $list = $this->getXpath()->query($this->getXpathPrefix() . '//atom:author'); + + if (!$list->length) { + /** + * TODO: Limit query to feed level els only! + */ + $list = $this->getXpath()->query('//atom:author'); + } + + if ($list->length) { + foreach ($list as $author) { + $author = $this->getAuthorFromElement($author); + if (!empty($author)) { + $authors[] = $author; + } + } + } + + if (count($authors) == 0) { + $authors = new Collection\Author(); + } else { + $authors = new Collection\Author( + Reader\Reader::arrayUnique($authors) + ); + } + + $this->data['authors'] = $authors; + return $this->data['authors']; + } + + /** + * Get the entry content + * + * @return string + */ + public function getContent() + { + if (array_key_exists('content', $this->data)) { + return $this->data['content']; + } + + $content = null; + + $el = $this->getXpath()->query($this->getXpathPrefix() . '/atom:content'); + if ($el->length > 0) { + $el = $el->item(0); + $type = $el->getAttribute('type'); + switch ($type) { + case '': + case 'text': + case 'text/plain': + case 'html': + case 'text/html': + $content = $el->nodeValue; + break; + case 'xhtml': + $this->getXpath()->registerNamespace('xhtml', 'http://www.w3.org/1999/xhtml'); + $xhtml = $this->getXpath()->query( + $this->getXpathPrefix() . '/atom:content/xhtml:div' + )->item(0); + $d = new DOMDocument('1.0', $this->getEncoding()); + $xhtmls = $d->importNode($xhtml, true); + $d->appendChild($xhtmls); + $content = $this->collectXhtml( + $d->saveXML(), + $d->lookupPrefix('http://www.w3.org/1999/xhtml') + ); + break; + } + } + + if (!$content) { + $content = $this->getDescription(); + } + + $this->data['content'] = trim($content); + + return $this->data['content']; + } + + /** + * Parse out XHTML to remove the namespacing + * + * @param $xhtml + * @param $prefix + * @return mixed + */ + protected function collectXhtml($xhtml, $prefix) + { + if (!empty($prefix)) { + $prefix = $prefix . ':'; + } + $matches = array( + "/<\?xml[^<]*>[^<]*<" . $prefix . "div[^<]*/", + "/<\/" . $prefix . "div>\s*$/" + ); + $xhtml = preg_replace($matches, '', $xhtml); + if (!empty($prefix)) { + $xhtml = preg_replace("/(<[\/]?)" . $prefix . "([a-zA-Z]+)/", '$1$2', $xhtml); + } + return $xhtml; + } + + /** + * Get the entry creation date + * + * @return string + */ + public function getDateCreated() + { + if (array_key_exists('datecreated', $this->data)) { + return $this->data['datecreated']; + } + + $date = null; + + if ($this->getAtomType() === Reader\Reader::TYPE_ATOM_03) { + $dateCreated = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:created)'); + } else { + $dateCreated = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:published)'); + } + + if ($dateCreated) { + $date = new DateTime($dateCreated); + } + + $this->data['datecreated'] = $date; + + return $this->data['datecreated']; + } + + /** + * Get the entry modification date + * + * @return string + */ + public function getDateModified() + { + if (array_key_exists('datemodified', $this->data)) { + return $this->data['datemodified']; + } + + $date = null; + + if ($this->getAtomType() === Reader\Reader::TYPE_ATOM_03) { + $dateModified = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:modified)'); + } else { + $dateModified = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:updated)'); + } + + if ($dateModified) { + $date = new DateTime($dateModified); + } + + $this->data['datemodified'] = $date; + + return $this->data['datemodified']; + } + + /** + * Get the entry description + * + * @return string + */ + public function getDescription() + { + if (array_key_exists('description', $this->data)) { + return $this->data['description']; + } + + $description = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:summary)'); + + if (!$description) { + $description = null; + } + + $this->data['description'] = $description; + + return $this->data['description']; + } + + /** + * Get the entry enclosure + * + * @return string + */ + public function getEnclosure() + { + if (array_key_exists('enclosure', $this->data)) { + return $this->data['enclosure']; + } + + $enclosure = null; + + $nodeList = $this->getXpath()->query($this->getXpathPrefix() . '/atom:link[@rel="enclosure"]'); + + if ($nodeList->length > 0) { + $enclosure = new stdClass(); + $enclosure->url = $nodeList->item(0)->getAttribute('href'); + $enclosure->length = $nodeList->item(0)->getAttribute('length'); + $enclosure->type = $nodeList->item(0)->getAttribute('type'); + } + + $this->data['enclosure'] = $enclosure; + + return $this->data['enclosure']; + } + + /** + * Get the entry ID + * + * @return string + */ + public function getId() + { + if (array_key_exists('id', $this->data)) { + return $this->data['id']; + } + + $id = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:id)'); + + if (!$id) { + if ($this->getPermalink()) { + $id = $this->getPermalink(); + } elseif ($this->getTitle()) { + $id = $this->getTitle(); + } else { + $id = null; + } + } + + $this->data['id'] = $id; + + return $this->data['id']; + } + + /** + * Get the base URI of the feed (if set). + * + * @return string|null + */ + public function getBaseUrl() + { + if (array_key_exists('baseUrl', $this->data)) { + return $this->data['baseUrl']; + } + + $baseUrl = $this->getXpath()->evaluate('string(' + . $this->getXpathPrefix() . '/@xml:base[1]' + . ')'); + + if (!$baseUrl) { + $baseUrl = $this->getXpath()->evaluate('string(//@xml:base[1])'); + } + + if (!$baseUrl) { + $baseUrl = null; + } + + $this->data['baseUrl'] = $baseUrl; + + return $this->data['baseUrl']; + } + + /** + * Get a specific link + * + * @param int $index + * @return string + */ + public function getLink($index = 0) + { + if (!array_key_exists('links', $this->data)) { + $this->getLinks(); + } + + if (isset($this->data['links'][$index])) { + return $this->data['links'][$index]; + } + + return null; + } + + /** + * Get all links + * + * @return array + */ + public function getLinks() + { + if (array_key_exists('links', $this->data)) { + return $this->data['links']; + } + + $links = array(); + + $list = $this->getXpath()->query( + $this->getXpathPrefix() . '//atom:link[@rel="alternate"]/@href' . '|' . + $this->getXpathPrefix() . '//atom:link[not(@rel)]/@href' + ); + + if ($list->length) { + foreach ($list as $link) { + $links[] = $this->absolutiseUri($link->value); + } + } + + $this->data['links'] = $links; + + return $this->data['links']; + } + + /** + * Get a permalink to the entry + * + * @return string + */ + public function getPermalink() + { + return $this->getLink(0); + } + + /** + * Get the entry title + * + * @return string + */ + public function getTitle() + { + if (array_key_exists('title', $this->data)) { + return $this->data['title']; + } + + $title = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:title)'); + + if (!$title) { + $title = null; + } + + $this->data['title'] = $title; + + return $this->data['title']; + } + + /** + * Get the number of comments/replies for current entry + * + * @return int + */ + public function getCommentCount() + { + if (array_key_exists('commentcount', $this->data)) { + return $this->data['commentcount']; + } + + $count = null; + + $this->getXpath()->registerNamespace('thread10', 'http://purl.org/syndication/thread/1.0'); + $list = $this->getXpath()->query( + $this->getXpathPrefix() . '//atom:link[@rel="replies"]/@thread10:count' + ); + + if ($list->length) { + $count = $list->item(0)->value; + } + + $this->data['commentcount'] = $count; + + return $this->data['commentcount']; + } + + /** + * Returns a URI pointing to the HTML page where comments can be made on this entry + * + * @return string + */ + public function getCommentLink() + { + if (array_key_exists('commentlink', $this->data)) { + return $this->data['commentlink']; + } + + $link = null; + + $list = $this->getXpath()->query( + $this->getXpathPrefix() . '//atom:link[@rel="replies" and @type="text/html"]/@href' + ); + + if ($list->length) { + $link = $list->item(0)->value; + $link = $this->absolutiseUri($link); + } + + $this->data['commentlink'] = $link; + + return $this->data['commentlink']; + } + + /** + * Returns a URI pointing to a feed of all comments for this entry + * + * @param string $type + * @return string + */ + public function getCommentFeedLink($type = 'atom') + { + if (array_key_exists('commentfeedlink', $this->data)) { + return $this->data['commentfeedlink']; + } + + $link = null; + + $list = $this->getXpath()->query( + $this->getXpathPrefix() . '//atom:link[@rel="replies" and @type="application/' . $type.'+xml"]/@href' + ); + + if ($list->length) { + $link = $list->item(0)->value; + $link = $this->absolutiseUri($link); + } + + $this->data['commentfeedlink'] = $link; + + return $this->data['commentfeedlink']; + } + + /** + * Get all categories + * + * @return Collection\Category + */ + public function getCategories() + { + if (array_key_exists('categories', $this->data)) { + return $this->data['categories']; + } + + if ($this->getAtomType() == Reader\Reader::TYPE_ATOM_10) { + $list = $this->getXpath()->query($this->getXpathPrefix() . '//atom:category'); + } else { + /** + * Since Atom 0.3 did not support categories, it would have used the + * Dublin Core extension. However there is a small possibility Atom 0.3 + * may have been retrofitted to use Atom 1.0 instead. + */ + $this->getXpath()->registerNamespace('atom10', Reader\Reader::NAMESPACE_ATOM_10); + $list = $this->getXpath()->query($this->getXpathPrefix() . '//atom10:category'); + } + + if ($list->length) { + $categoryCollection = new Collection\Category; + foreach ($list as $category) { + $categoryCollection[] = array( + 'term' => $category->getAttribute('term'), + 'scheme' => $category->getAttribute('scheme'), + 'label' => $category->getAttribute('label') + ); + } + } else { + return new Collection\Category; + } + + $this->data['categories'] = $categoryCollection; + + return $this->data['categories']; + } + + /** + * Get source feed metadata from the entry + * + * @return Reader\Feed\Atom\Source|null + */ + public function getSource() + { + if (array_key_exists('source', $this->data)) { + return $this->data['source']; + } + + $source = null; + // TODO: Investigate why _getAtomType() fails here. Is it even needed? + if ($this->getType() == Reader\Reader::TYPE_ATOM_10) { + $list = $this->getXpath()->query($this->getXpathPrefix() . '/atom:source[1]'); + if ($list->length) { + $element = $list->item(0); + $source = new Reader\Feed\Atom\Source($element, $this->getXpathPrefix()); + } + } + + $this->data['source'] = $source; + return $this->data['source']; + } + + /** + * Attempt to absolutise the URI, i.e. if a relative URI apply the + * xml:base value as a prefix to turn into an absolute URI. + * + * @param $link + * @return string + */ + protected function absolutiseUri($link) + { + if (!Uri::factory($link)->isAbsolute()) { + if ($this->getBaseUrl() !== null) { + $link = $this->getBaseUrl() . $link; + if (!Uri::factory($link)->isValid()) { + $link = null; + } + } + } + return $link; + } + + /** + * Get an author entry + * + * @param DOMElement $element + * @return string + */ + protected function getAuthorFromElement(DOMElement $element) + { + $author = array(); + + $emailNode = $element->getElementsByTagName('email'); + $nameNode = $element->getElementsByTagName('name'); + $uriNode = $element->getElementsByTagName('uri'); + + if ($emailNode->length && strlen($emailNode->item(0)->nodeValue) > 0) { + $author['email'] = $emailNode->item(0)->nodeValue; + } + + if ($nameNode->length && strlen($nameNode->item(0)->nodeValue) > 0) { + $author['name'] = $nameNode->item(0)->nodeValue; + } + + if ($uriNode->length && strlen($uriNode->item(0)->nodeValue) > 0) { + $author['uri'] = $uriNode->item(0)->nodeValue; + } + + if (empty($author)) { + return null; + } + return $author; + } + + /** + * Register the default namespaces for the current feed format + */ + protected function registerNamespaces() + { + switch ($this->getAtomType()) { + case Reader\Reader::TYPE_ATOM_03: + $this->getXpath()->registerNamespace('atom', Reader\Reader::NAMESPACE_ATOM_03); + break; + default: + $this->getXpath()->registerNamespace('atom', Reader\Reader::NAMESPACE_ATOM_10); + break; + } + } + + /** + * Detect the presence of any Atom namespaces in use + * + * @return string + */ + protected function getAtomType() + { + $dom = $this->getDomDocument(); + $prefixAtom03 = $dom->lookupPrefix(Reader\Reader::NAMESPACE_ATOM_03); + $prefixAtom10 = $dom->lookupPrefix(Reader\Reader::NAMESPACE_ATOM_10); + if ($dom->isDefaultNamespace(Reader\Reader::NAMESPACE_ATOM_03) + || !empty($prefixAtom03)) { + return Reader\Reader::TYPE_ATOM_03; + } + if ($dom->isDefaultNamespace(Reader\Reader::NAMESPACE_ATOM_10) + || !empty($prefixAtom10)) { + return Reader\Reader::TYPE_ATOM_10; + } + } +} diff --git a/library/Zend/Feed/Reader/Extension/Atom/Feed.php b/library/Zend/Feed/Reader/Extension/Atom/Feed.php new file mode 100755 index 0000000000..986d23fdb3 --- /dev/null +++ b/library/Zend/Feed/Reader/Extension/Atom/Feed.php @@ -0,0 +1,536 @@ +getAuthors(); + + if (isset($authors[$index])) { + return $authors[$index]; + } + + return null; + } + + /** + * Get an array with feed authors + * + * @return Collection\Author + */ + public function getAuthors() + { + if (array_key_exists('authors', $this->data)) { + return $this->data['authors']; + } + + $list = $this->xpath->query('//atom:author'); + + $authors = array(); + + if ($list->length) { + foreach ($list as $author) { + $author = $this->getAuthorFromElement($author); + if (!empty($author)) { + $authors[] = $author; + } + } + } + + if (count($authors) == 0) { + $authors = new Collection\Author(); + } else { + $authors = new Collection\Author( + Reader\Reader::arrayUnique($authors) + ); + } + + $this->data['authors'] = $authors; + + return $this->data['authors']; + } + + /** + * Get the copyright entry + * + * @return string|null + */ + public function getCopyright() + { + if (array_key_exists('copyright', $this->data)) { + return $this->data['copyright']; + } + + $copyright = null; + + if ($this->getType() === Reader\Reader::TYPE_ATOM_03) { + $copyright = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:copyright)'); + } else { + $copyright = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:rights)'); + } + + if (!$copyright) { + $copyright = null; + } + + $this->data['copyright'] = $copyright; + + return $this->data['copyright']; + } + + /** + * Get the feed creation date + * + * @return DateTime|null + */ + public function getDateCreated() + { + if (array_key_exists('datecreated', $this->data)) { + return $this->data['datecreated']; + } + + $date = null; + + if ($this->getType() === Reader\Reader::TYPE_ATOM_03) { + $dateCreated = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:created)'); + } else { + $dateCreated = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:published)'); + } + + if ($dateCreated) { + $date = new DateTime($dateCreated); + } + + $this->data['datecreated'] = $date; + + return $this->data['datecreated']; + } + + /** + * Get the feed modification date + * + * @return DateTime|null + */ + public function getDateModified() + { + if (array_key_exists('datemodified', $this->data)) { + return $this->data['datemodified']; + } + + $date = null; + + if ($this->getType() === Reader\Reader::TYPE_ATOM_03) { + $dateModified = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:modified)'); + } else { + $dateModified = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:updated)'); + } + + if ($dateModified) { + $date = new DateTime($dateModified); + } + + $this->data['datemodified'] = $date; + + return $this->data['datemodified']; + } + + /** + * Get the feed description + * + * @return string|null + */ + public function getDescription() + { + if (array_key_exists('description', $this->data)) { + return $this->data['description']; + } + + $description = null; + + if ($this->getType() === Reader\Reader::TYPE_ATOM_03) { + $description = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:tagline)'); + } else { + $description = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:subtitle)'); + } + + if (!$description) { + $description = null; + } + + $this->data['description'] = $description; + + return $this->data['description']; + } + + /** + * Get the feed generator entry + * + * @return string|null + */ + public function getGenerator() + { + if (array_key_exists('generator', $this->data)) { + return $this->data['generator']; + } + // TODO: Add uri support + $generator = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:generator)'); + + if (!$generator) { + $generator = null; + } + + $this->data['generator'] = $generator; + + return $this->data['generator']; + } + + /** + * Get the feed ID + * + * @return string|null + */ + public function getId() + { + if (array_key_exists('id', $this->data)) { + return $this->data['id']; + } + + $id = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:id)'); + + if (!$id) { + if ($this->getLink()) { + $id = $this->getLink(); + } elseif ($this->getTitle()) { + $id = $this->getTitle(); + } else { + $id = null; + } + } + + $this->data['id'] = $id; + + return $this->data['id']; + } + + /** + * Get the feed language + * + * @return string|null + */ + public function getLanguage() + { + if (array_key_exists('language', $this->data)) { + return $this->data['language']; + } + + $language = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:lang)'); + + if (!$language) { + $language = $this->xpath->evaluate('string(//@xml:lang[1])'); + } + + if (!$language) { + $language = null; + } + + $this->data['language'] = $language; + + return $this->data['language']; + } + + /** + * Get the feed image + * + * @return array|null + */ + public function getImage() + { + if (array_key_exists('image', $this->data)) { + return $this->data['image']; + } + + $imageUrl = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:logo)'); + + if (!$imageUrl) { + $image = null; + } else { + $image = array('uri' => $imageUrl); + } + + $this->data['image'] = $image; + + return $this->data['image']; + } + + /** + * Get the base URI of the feed (if set). + * + * @return string|null + */ + public function getBaseUrl() + { + if (array_key_exists('baseUrl', $this->data)) { + return $this->data['baseUrl']; + } + + $baseUrl = $this->xpath->evaluate('string(//@xml:base[1])'); + + if (!$baseUrl) { + $baseUrl = null; + } + $this->data['baseUrl'] = $baseUrl; + + return $this->data['baseUrl']; + } + + /** + * Get a link to the source website + * + * @return string|null + */ + public function getLink() + { + if (array_key_exists('link', $this->data)) { + return $this->data['link']; + } + + $link = null; + + $list = $this->xpath->query( + $this->getXpathPrefix() . '/atom:link[@rel="alternate"]/@href' . '|' . + $this->getXpathPrefix() . '/atom:link[not(@rel)]/@href' + ); + + if ($list->length) { + $link = $list->item(0)->nodeValue; + $link = $this->absolutiseUri($link); + } + + $this->data['link'] = $link; + + return $this->data['link']; + } + + /** + * Get a link to the feed's XML Url + * + * @return string|null + */ + public function getFeedLink() + { + if (array_key_exists('feedlink', $this->data)) { + return $this->data['feedlink']; + } + + $link = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:link[@rel="self"]/@href)'); + + $link = $this->absolutiseUri($link); + + $this->data['feedlink'] = $link; + + return $this->data['feedlink']; + } + + /** + * Get an array of any supported Pusubhubbub endpoints + * + * @return array|null + */ + public function getHubs() + { + if (array_key_exists('hubs', $this->data)) { + return $this->data['hubs']; + } + $hubs = array(); + + $list = $this->xpath->query($this->getXpathPrefix() + . '//atom:link[@rel="hub"]/@href'); + + if ($list->length) { + foreach ($list as $uri) { + $hubs[] = $this->absolutiseUri($uri->nodeValue); + } + } else { + $hubs = null; + } + + $this->data['hubs'] = $hubs; + + return $this->data['hubs']; + } + + /** + * Get the feed title + * + * @return string|null + */ + public function getTitle() + { + if (array_key_exists('title', $this->data)) { + return $this->data['title']; + } + + $title = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:title)'); + + if (!$title) { + $title = null; + } + + $this->data['title'] = $title; + + return $this->data['title']; + } + + /** + * Get all categories + * + * @return Collection\Category + */ + public function getCategories() + { + if (array_key_exists('categories', $this->data)) { + return $this->data['categories']; + } + + if ($this->getType() == Reader\Reader::TYPE_ATOM_10) { + $list = $this->xpath->query($this->getXpathPrefix() . '/atom:category'); + } else { + /** + * Since Atom 0.3 did not support categories, it would have used the + * Dublin Core extension. However there is a small possibility Atom 0.3 + * may have been retrofittied to use Atom 1.0 instead. + */ + $this->xpath->registerNamespace('atom10', Reader\Reader::NAMESPACE_ATOM_10); + $list = $this->xpath->query($this->getXpathPrefix() . '/atom10:category'); + } + + if ($list->length) { + $categoryCollection = new Collection\Category; + foreach ($list as $category) { + $categoryCollection[] = array( + 'term' => $category->getAttribute('term'), + 'scheme' => $category->getAttribute('scheme'), + 'label' => $category->getAttribute('label') + ); + } + } else { + return new Collection\Category; + } + + $this->data['categories'] = $categoryCollection; + + return $this->data['categories']; + } + + /** + * Get an author entry in RSS format + * + * @param DOMElement $element + * @return string + */ + protected function getAuthorFromElement(DOMElement $element) + { + $author = array(); + + $emailNode = $element->getElementsByTagName('email'); + $nameNode = $element->getElementsByTagName('name'); + $uriNode = $element->getElementsByTagName('uri'); + + if ($emailNode->length && strlen($emailNode->item(0)->nodeValue) > 0) { + $author['email'] = $emailNode->item(0)->nodeValue; + } + + if ($nameNode->length && strlen($nameNode->item(0)->nodeValue) > 0) { + $author['name'] = $nameNode->item(0)->nodeValue; + } + + if ($uriNode->length && strlen($uriNode->item(0)->nodeValue) > 0) { + $author['uri'] = $uriNode->item(0)->nodeValue; + } + + if (empty($author)) { + return null; + } + return $author; + } + + /** + * Attempt to absolutise the URI, i.e. if a relative URI apply the + * xml:base value as a prefix to turn into an absolute URI. + */ + protected function absolutiseUri($link) + { + if (!Uri::factory($link)->isAbsolute()) { + if ($this->getBaseUrl() !== null) { + $link = $this->getBaseUrl() . $link; + if (!Uri::factory($link)->isValid()) { + $link = null; + } + } + } + return $link; + } + + /** + * Register the default namespaces for the current feed format + */ + protected function registerNamespaces() + { + if ($this->getType() == Reader\Reader::TYPE_ATOM_10 + || $this->getType() == Reader\Reader::TYPE_ATOM_03 + ) { + return; // pre-registered at Feed level + } + $atomDetected = $this->getAtomType(); + switch ($atomDetected) { + case Reader\Reader::TYPE_ATOM_03: + $this->xpath->registerNamespace('atom', Reader\Reader::NAMESPACE_ATOM_03); + break; + default: + $this->xpath->registerNamespace('atom', Reader\Reader::NAMESPACE_ATOM_10); + break; + } + } + + /** + * Detect the presence of any Atom namespaces in use + */ + protected function getAtomType() + { + $dom = $this->getDomDocument(); + $prefixAtom03 = $dom->lookupPrefix(Reader\Reader::NAMESPACE_ATOM_03); + $prefixAtom10 = $dom->lookupPrefix(Reader\Reader::NAMESPACE_ATOM_10); + if ($dom->isDefaultNamespace(Reader\Reader::NAMESPACE_ATOM_10) + || !empty($prefixAtom10) + ) { + return Reader\Reader::TYPE_ATOM_10; + } + if ($dom->isDefaultNamespace(Reader\Reader::NAMESPACE_ATOM_03) + || !empty($prefixAtom03) + ) { + return Reader\Reader::TYPE_ATOM_03; + } + } +} diff --git a/library/Zend/Feed/Reader/Extension/Content/Entry.php b/library/Zend/Feed/Reader/Extension/Content/Entry.php new file mode 100755 index 0000000000..9b5f7cb355 --- /dev/null +++ b/library/Zend/Feed/Reader/Extension/Content/Entry.php @@ -0,0 +1,36 @@ +getType() !== Reader\Reader::TYPE_RSS_10 + && $this->getType() !== Reader\Reader::TYPE_RSS_090 + ) { + $content = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/content:encoded)'); + } else { + $content = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/content:encoded)'); + } + return $content; + } + + /** + * Register RSS Content Module namespace + */ + protected function registerNamespaces() + { + $this->xpath->registerNamespace('content', 'http://purl.org/rss/1.0/modules/content/'); + } +} diff --git a/library/Zend/Feed/Reader/Extension/CreativeCommons/Entry.php b/library/Zend/Feed/Reader/Extension/CreativeCommons/Entry.php new file mode 100755 index 0000000000..1883dc6bed --- /dev/null +++ b/library/Zend/Feed/Reader/Extension/CreativeCommons/Entry.php @@ -0,0 +1,72 @@ +getLicenses(); + + if (isset($licenses[$index])) { + return $licenses[$index]; + } + + return null; + } + + /** + * Get the entry licenses + * + * @return array + */ + public function getLicenses() + { + $name = 'licenses'; + if (array_key_exists($name, $this->data)) { + return $this->data[$name]; + } + + $licenses = array(); + $list = $this->xpath->evaluate($this->getXpathPrefix() . '//cc:license'); + + if ($list->length) { + foreach ($list as $license) { + $licenses[] = $license->nodeValue; + } + + $licenses = array_unique($licenses); + } else { + $cc = new Feed(); + $licenses = $cc->getLicenses(); + } + + $this->data[$name] = $licenses; + + return $this->data[$name]; + } + + /** + * Register Creative Commons namespaces + * + */ + protected function registerNamespaces() + { + $this->xpath->registerNamespace('cc', 'http://backend.userland.com/creativeCommonsRssModule'); + } +} diff --git a/library/Zend/Feed/Reader/Extension/CreativeCommons/Feed.php b/library/Zend/Feed/Reader/Extension/CreativeCommons/Feed.php new file mode 100755 index 0000000000..99977fd44a --- /dev/null +++ b/library/Zend/Feed/Reader/Extension/CreativeCommons/Feed.php @@ -0,0 +1,70 @@ +getLicenses(); + + if (isset($licenses[$index])) { + return $licenses[$index]; + } + + return null; + } + + /** + * Get the entry licenses + * + * @return array + */ + public function getLicenses() + { + $name = 'licenses'; + if (array_key_exists($name, $this->data)) { + return $this->data[$name]; + } + + $licenses = array(); + $list = $this->xpath->evaluate('channel/cc:license'); + + if ($list->length) { + foreach ($list as $license) { + $licenses[] = $license->nodeValue; + } + + $licenses = array_unique($licenses); + } + + $this->data[$name] = $licenses; + + return $this->data[$name]; + } + + /** + * Register Creative Commons namespaces + * + * @return void + */ + protected function registerNamespaces() + { + $this->xpath->registerNamespace('cc', 'http://backend.userland.com/creativeCommonsRssModule'); + } +} diff --git a/library/Zend/Feed/Reader/Extension/DublinCore/Entry.php b/library/Zend/Feed/Reader/Extension/DublinCore/Entry.php new file mode 100755 index 0000000000..2713353cad --- /dev/null +++ b/library/Zend/Feed/Reader/Extension/DublinCore/Entry.php @@ -0,0 +1,238 @@ +getAuthors(); + + if (isset($authors[$index])) { + return $authors[$index]; + } + + return null; + } + + /** + * Get an array with feed authors + * + * @return array + */ + public function getAuthors() + { + if (array_key_exists('authors', $this->data)) { + return $this->data['authors']; + } + + $authors = array(); + $list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc11:creator'); + + if (!$list->length) { + $list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc10:creator'); + } + if (!$list->length) { + $list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc11:publisher'); + + if (!$list->length) { + $list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc10:publisher'); + } + } + + if ($list->length) { + foreach ($list as $author) { + $authors[] = array( + 'name' => $author->nodeValue + ); + } + $authors = new Collection\Author( + Reader\Reader::arrayUnique($authors) + ); + } else { + $authors = null; + } + + $this->data['authors'] = $authors; + + return $this->data['authors']; + } + + /** + * Get categories (subjects under DC) + * + * @return Collection\Category + */ + public function getCategories() + { + if (array_key_exists('categories', $this->data)) { + return $this->data['categories']; + } + + $list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc11:subject'); + + if (!$list->length) { + $list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc10:subject'); + } + + if ($list->length) { + $categoryCollection = new Collection\Category; + foreach ($list as $category) { + $categoryCollection[] = array( + 'term' => $category->nodeValue, + 'scheme' => null, + 'label' => $category->nodeValue, + ); + } + } else { + $categoryCollection = new Collection\Category; + } + + $this->data['categories'] = $categoryCollection; + return $this->data['categories']; + } + + + /** + * Get the entry content + * + * @return string + */ + public function getContent() + { + return $this->getDescription(); + } + + /** + * Get the entry description + * + * @return string + */ + public function getDescription() + { + if (array_key_exists('description', $this->data)) { + return $this->data['description']; + } + + $description = null; + $description = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:description)'); + + if (!$description) { + $description = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:description)'); + } + + if (!$description) { + $description = null; + } + + $this->data['description'] = $description; + + return $this->data['description']; + } + + /** + * Get the entry ID + * + * @return string + */ + public function getId() + { + if (array_key_exists('id', $this->data)) { + return $this->data['id']; + } + + $id = null; + $id = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:identifier)'); + + if (!$id) { + $id = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:identifier)'); + } + + $this->data['id'] = $id; + + return $this->data['id']; + } + + /** + * Get the entry title + * + * @return string + */ + public function getTitle() + { + if (array_key_exists('title', $this->data)) { + return $this->data['title']; + } + + $title = null; + $title = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:title)'); + + if (!$title) { + $title = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:title)'); + } + + if (!$title) { + $title = null; + } + + $this->data['title'] = $title; + + return $this->data['title']; + } + + /** + * + * + * @return DateTime|null + */ + public function getDate() + { + if (array_key_exists('date', $this->data)) { + return $this->data['date']; + } + + $d = null; + $date = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:date)'); + + if (!$date) { + $date = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:date)'); + } + + if ($date) { + $d = new DateTime($date); + } + + $this->data['date'] = $d; + + return $this->data['date']; + } + + /** + * Register DC namespaces + * + * @return void + */ + protected function registerNamespaces() + { + $this->getXpath()->registerNamespace('dc10', 'http://purl.org/dc/elements/1.0/'); + $this->getXpath()->registerNamespace('dc11', 'http://purl.org/dc/elements/1.1/'); + } +} diff --git a/library/Zend/Feed/Reader/Extension/DublinCore/Feed.php b/library/Zend/Feed/Reader/Extension/DublinCore/Feed.php new file mode 100755 index 0000000000..2738ac732b --- /dev/null +++ b/library/Zend/Feed/Reader/Extension/DublinCore/Feed.php @@ -0,0 +1,281 @@ +getAuthors(); + + if (isset($authors[$index])) { + return $authors[$index]; + } + + return null; + } + + /** + * Get an array with feed authors + * + * @return array + */ + public function getAuthors() + { + if (array_key_exists('authors', $this->data)) { + return $this->data['authors']; + } + + $authors = array(); + $list = $this->getXpath()->query('//dc11:creator'); + + if (!$list->length) { + $list = $this->getXpath()->query('//dc10:creator'); + } + if (!$list->length) { + $list = $this->getXpath()->query('//dc11:publisher'); + + if (!$list->length) { + $list = $this->getXpath()->query('//dc10:publisher'); + } + } + + if ($list->length) { + foreach ($list as $author) { + $authors[] = array( + 'name' => $author->nodeValue + ); + } + $authors = new Collection\Author( + Reader\Reader::arrayUnique($authors) + ); + } else { + $authors = null; + } + + $this->data['authors'] = $authors; + + return $this->data['authors']; + } + + /** + * Get the copyright entry + * + * @return string|null + */ + public function getCopyright() + { + if (array_key_exists('copyright', $this->data)) { + return $this->data['copyright']; + } + + $copyright = null; + $copyright = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:rights)'); + + if (!$copyright) { + $copyright = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:rights)'); + } + + if (!$copyright) { + $copyright = null; + } + + $this->data['copyright'] = $copyright; + + return $this->data['copyright']; + } + + /** + * Get the feed description + * + * @return string|null + */ + public function getDescription() + { + if (array_key_exists('description', $this->data)) { + return $this->data['description']; + } + + $description = null; + $description = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:description)'); + + if (!$description) { + $description = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:description)'); + } + + if (!$description) { + $description = null; + } + + $this->data['description'] = $description; + + return $this->data['description']; + } + + /** + * Get the feed ID + * + * @return string|null + */ + public function getId() + { + if (array_key_exists('id', $this->data)) { + return $this->data['id']; + } + + $id = null; + $id = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:identifier)'); + + if (!$id) { + $id = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:identifier)'); + } + + $this->data['id'] = $id; + + return $this->data['id']; + } + + /** + * Get the feed language + * + * @return string|null + */ + public function getLanguage() + { + if (array_key_exists('language', $this->data)) { + return $this->data['language']; + } + + $language = null; + $language = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:language)'); + + if (!$language) { + $language = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:language)'); + } + + if (!$language) { + $language = null; + } + + $this->data['language'] = $language; + + return $this->data['language']; + } + + /** + * Get the feed title + * + * @return string|null + */ + public function getTitle() + { + if (array_key_exists('title', $this->data)) { + return $this->data['title']; + } + + $title = null; + $title = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:title)'); + + if (!$title) { + $title = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:title)'); + } + + if (!$title) { + $title = null; + } + + $this->data['title'] = $title; + + return $this->data['title']; + } + + /** + * + * + * @return DateTime|null + */ + public function getDate() + { + if (array_key_exists('date', $this->data)) { + return $this->data['date']; + } + + $d = null; + $date = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:date)'); + + if (!$date) { + $date = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:date)'); + } + + if ($date) { + $d = new DateTime($date); + } + + $this->data['date'] = $d; + + return $this->data['date']; + } + + /** + * Get categories (subjects under DC) + * + * @return Collection\Category + */ + public function getCategories() + { + if (array_key_exists('categories', $this->data)) { + return $this->data['categories']; + } + + $list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc11:subject'); + + if (!$list->length) { + $list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc10:subject'); + } + + if ($list->length) { + $categoryCollection = new Collection\Category; + foreach ($list as $category) { + $categoryCollection[] = array( + 'term' => $category->nodeValue, + 'scheme' => null, + 'label' => $category->nodeValue, + ); + } + } else { + $categoryCollection = new Collection\Category; + } + + $this->data['categories'] = $categoryCollection; + return $this->data['categories']; + } + + /** + * Register the default namespaces for the current feed format + * + * @return void + */ + protected function registerNamespaces() + { + $this->getXpath()->registerNamespace('dc10', 'http://purl.org/dc/elements/1.0/'); + $this->getXpath()->registerNamespace('dc11', 'http://purl.org/dc/elements/1.1/'); + } +} diff --git a/library/Zend/Feed/Reader/Extension/Podcast/Entry.php b/library/Zend/Feed/Reader/Extension/Podcast/Entry.php new file mode 100755 index 0000000000..c97e64ff47 --- /dev/null +++ b/library/Zend/Feed/Reader/Extension/Podcast/Entry.php @@ -0,0 +1,180 @@ +data['author'])) { + return $this->data['author']; + } + + $author = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:author)'); + + if (!$author) { + $author = null; + } + + $this->data['author'] = $author; + + return $this->data['author']; + } + + /** + * Get the entry block + * + * @return string + */ + public function getBlock() + { + if (isset($this->data['block'])) { + return $this->data['block']; + } + + $block = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:block)'); + + if (!$block) { + $block = null; + } + + $this->data['block'] = $block; + + return $this->data['block']; + } + + /** + * Get the entry duration + * + * @return string + */ + public function getDuration() + { + if (isset($this->data['duration'])) { + return $this->data['duration']; + } + + $duration = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:duration)'); + + if (!$duration) { + $duration = null; + } + + $this->data['duration'] = $duration; + + return $this->data['duration']; + } + + /** + * Get the entry explicit + * + * @return string + */ + public function getExplicit() + { + if (isset($this->data['explicit'])) { + return $this->data['explicit']; + } + + $explicit = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:explicit)'); + + if (!$explicit) { + $explicit = null; + } + + $this->data['explicit'] = $explicit; + + return $this->data['explicit']; + } + + /** + * Get the entry keywords + * + * @return string + */ + public function getKeywords() + { + if (isset($this->data['keywords'])) { + return $this->data['keywords']; + } + + $keywords = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:keywords)'); + + if (!$keywords) { + $keywords = null; + } + + $this->data['keywords'] = $keywords; + + return $this->data['keywords']; + } + + /** + * Get the entry subtitle + * + * @return string + */ + public function getSubtitle() + { + if (isset($this->data['subtitle'])) { + return $this->data['subtitle']; + } + + $subtitle = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:subtitle)'); + + if (!$subtitle) { + $subtitle = null; + } + + $this->data['subtitle'] = $subtitle; + + return $this->data['subtitle']; + } + + /** + * Get the entry summary + * + * @return string + */ + public function getSummary() + { + if (isset($this->data['summary'])) { + return $this->data['summary']; + } + + $summary = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:summary)'); + + if (!$summary) { + $summary = null; + } + + $this->data['summary'] = $summary; + + return $this->data['summary']; + } + + /** + * Register iTunes namespace + * + */ + protected function registerNamespaces() + { + $this->xpath->registerNamespace('itunes', 'http://www.itunes.com/dtds/podcast-1.0.dtd'); + } +} diff --git a/library/Zend/Feed/Reader/Extension/Podcast/Feed.php b/library/Zend/Feed/Reader/Extension/Podcast/Feed.php new file mode 100755 index 0000000000..66b13a48b2 --- /dev/null +++ b/library/Zend/Feed/Reader/Extension/Podcast/Feed.php @@ -0,0 +1,277 @@ +data['author'])) { + return $this->data['author']; + } + + $author = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:author)'); + + if (!$author) { + $author = null; + } + + $this->data['author'] = $author; + + return $this->data['author']; + } + + /** + * Get the entry block + * + * @return string + */ + public function getBlock() + { + if (isset($this->data['block'])) { + return $this->data['block']; + } + + $block = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:block)'); + + if (!$block) { + $block = null; + } + + $this->data['block'] = $block; + + return $this->data['block']; + } + + /** + * Get the entry category + * + * @return string + */ + public function getItunesCategories() + { + if (isset($this->data['categories'])) { + return $this->data['categories']; + } + + $categoryList = $this->xpath->query($this->getXpathPrefix() . '/itunes:category'); + + $categories = array(); + + if ($categoryList->length > 0) { + foreach ($categoryList as $node) { + $children = null; + + if ($node->childNodes->length > 0) { + $children = array(); + + foreach ($node->childNodes as $childNode) { + if (!($childNode instanceof DOMText)) { + $children[$childNode->getAttribute('text')] = null; + } + } + } + + $categories[$node->getAttribute('text')] = $children; + } + } + + + if (!$categories) { + $categories = null; + } + + $this->data['categories'] = $categories; + + return $this->data['categories']; + } + + /** + * Get the entry explicit + * + * @return string + */ + public function getExplicit() + { + if (isset($this->data['explicit'])) { + return $this->data['explicit']; + } + + $explicit = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:explicit)'); + + if (!$explicit) { + $explicit = null; + } + + $this->data['explicit'] = $explicit; + + return $this->data['explicit']; + } + + /** + * Get the entry image + * + * @return string + */ + public function getItunesImage() + { + if (isset($this->data['image'])) { + return $this->data['image']; + } + + $image = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:image/@href)'); + + if (!$image) { + $image = null; + } + + $this->data['image'] = $image; + + return $this->data['image']; + } + + /** + * Get the entry keywords + * + * @return string + */ + public function getKeywords() + { + if (isset($this->data['keywords'])) { + return $this->data['keywords']; + } + + $keywords = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:keywords)'); + + if (!$keywords) { + $keywords = null; + } + + $this->data['keywords'] = $keywords; + + return $this->data['keywords']; + } + + /** + * Get the entry's new feed url + * + * @return string + */ + public function getNewFeedUrl() + { + if (isset($this->data['new-feed-url'])) { + return $this->data['new-feed-url']; + } + + $newFeedUrl = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:new-feed-url)'); + + if (!$newFeedUrl) { + $newFeedUrl = null; + } + + $this->data['new-feed-url'] = $newFeedUrl; + + return $this->data['new-feed-url']; + } + + /** + * Get the entry owner + * + * @return string + */ + public function getOwner() + { + if (isset($this->data['owner'])) { + return $this->data['owner']; + } + + $owner = null; + + $email = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:owner/itunes:email)'); + $name = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:owner/itunes:name)'); + + if (!empty($email)) { + $owner = $email . (empty($name) ? '' : ' (' . $name . ')'); + } elseif (!empty($name)) { + $owner = $name; + } + + if (!$owner) { + $owner = null; + } + + $this->data['owner'] = $owner; + + return $this->data['owner']; + } + + /** + * Get the entry subtitle + * + * @return string + */ + public function getSubtitle() + { + if (isset($this->data['subtitle'])) { + return $this->data['subtitle']; + } + + $subtitle = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:subtitle)'); + + if (!$subtitle) { + $subtitle = null; + } + + $this->data['subtitle'] = $subtitle; + + return $this->data['subtitle']; + } + + /** + * Get the entry summary + * + * @return string + */ + public function getSummary() + { + if (isset($this->data['summary'])) { + return $this->data['summary']; + } + + $summary = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:summary)'); + + if (!$summary) { + $summary = null; + } + + $this->data['summary'] = $summary; + + return $this->data['summary']; + } + + /** + * Register iTunes namespace + * + */ + protected function registerNamespaces() + { + $this->xpath->registerNamespace('itunes', 'http://www.itunes.com/dtds/podcast-1.0.dtd'); + } +} diff --git a/library/Zend/Feed/Reader/Extension/Slash/Entry.php b/library/Zend/Feed/Reader/Extension/Slash/Entry.php new file mode 100755 index 0000000000..9ddb862e23 --- /dev/null +++ b/library/Zend/Feed/Reader/Extension/Slash/Entry.php @@ -0,0 +1,122 @@ +getData('section'); + } + + /** + * Get the entry department + * + * @return string|null + */ + public function getDepartment() + { + return $this->getData('department'); + } + + /** + * Get the entry hit_parade + * + * @return array + */ + public function getHitParade() + { + $name = 'hit_parade'; + + if (isset($this->data[$name])) { + return $this->data[$name]; + } + + $stringParade = $this->getData($name); + $hitParade = array(); + + if (!empty($stringParade)) { + $stringParade = explode(',', $stringParade); + + foreach ($stringParade as $hit) { + $hitParade[] = $hit + 0; //cast to integer + } + } + + $this->data[$name] = $hitParade; + return $hitParade; + } + + /** + * Get the entry comments + * + * @return int + */ + public function getCommentCount() + { + $name = 'comments'; + + if (isset($this->data[$name])) { + return $this->data[$name]; + } + + $comments = $this->getData($name, 'string'); + + if (!$comments) { + $this->data[$name] = null; + return $this->data[$name]; + } + + return $comments; + } + + /** + * Get the entry data specified by name + * @param string $name + * @param string $type + * + * @return mixed|null + */ + protected function getData($name, $type = 'string') + { + if (array_key_exists($name, $this->data)) { + return $this->data[$name]; + } + + $data = $this->xpath->evaluate($type . '(' . $this->getXpathPrefix() . '/slash10:' . $name . ')'); + + if (!$data) { + $data = null; + } + + $this->data[$name] = $data; + + return $data; + } + + /** + * Register Slash namespaces + * + * @return void + */ + protected function registerNamespaces() + { + $this->xpath->registerNamespace('slash10', 'http://purl.org/rss/1.0/modules/slash/'); + } +} diff --git a/library/Zend/Feed/Reader/Extension/Syndication/Feed.php b/library/Zend/Feed/Reader/Extension/Syndication/Feed.php new file mode 100755 index 0000000000..db1724c14c --- /dev/null +++ b/library/Zend/Feed/Reader/Extension/Syndication/Feed.php @@ -0,0 +1,151 @@ +getData($name); + + if ($period === null) { + $this->data[$name] = 'daily'; + return 'daily'; //Default specified by spec + } + + switch ($period) { + case 'hourly': + case 'daily': + case 'weekly': + case 'yearly': + return $period; + default: + throw new Reader\Exception\InvalidArgumentException("Feed specified invalid update period: '$period'." + . " Must be one of hourly, daily, weekly or yearly" + ); + } + } + + /** + * Get update frequency + * + * @return int + */ + public function getUpdateFrequency() + { + $name = 'updateFrequency'; + $freq = $this->getData($name, 'number'); + + if (!$freq || $freq < 1) { + $this->data[$name] = 1; + return 1; + } + + return $freq; + } + + /** + * Get update frequency as ticks + * + * @return int + */ + public function getUpdateFrequencyAsTicks() + { + $name = 'updateFrequency'; + $freq = $this->getData($name, 'number'); + + if (!$freq || $freq < 1) { + $this->data[$name] = 1; + $freq = 1; + } + + $period = $this->getUpdatePeriod(); + $ticks = 1; + + switch ($period) { + case 'yearly': + $ticks *= 52; //TODO: fix generalisation, how? + // no break + case 'weekly': + $ticks *= 7; + // no break + case 'daily': + $ticks *= 24; + // no break + case 'hourly': + $ticks *= 3600; + break; + default: //Never arrive here, exception thrown in getPeriod() + break; + } + + return $ticks / $freq; + } + + /** + * Get update base + * + * @return DateTime|null + */ + public function getUpdateBase() + { + $updateBase = $this->getData('updateBase'); + $date = null; + if ($updateBase) { + $date = DateTime::createFromFormat(DateTime::W3C, $updateBase); + } + return $date; + } + + /** + * Get the entry data specified by name + * + * @param string $name + * @param string $type + * @return mixed|null + */ + private function getData($name, $type = 'string') + { + if (array_key_exists($name, $this->data)) { + return $this->data[$name]; + } + + $data = $this->xpath->evaluate($type . '(' . $this->getXpathPrefix() . '/syn10:' . $name . ')'); + + if (!$data) { + $data = null; + } + + $this->data[$name] = $data; + + return $data; + } + + /** + * Register Syndication namespaces + * + * @return void + */ + protected function registerNamespaces() + { + $this->xpath->registerNamespace('syn10', 'http://purl.org/rss/1.0/modules/syndication/'); + } +} diff --git a/library/Zend/Feed/Reader/Extension/Thread/Entry.php b/library/Zend/Feed/Reader/Extension/Thread/Entry.php new file mode 100755 index 0000000000..d3bc315871 --- /dev/null +++ b/library/Zend/Feed/Reader/Extension/Thread/Entry.php @@ -0,0 +1,72 @@ +getData('total'); + } + + /** + * Get the entry data specified by name + * + * @param string $name + * @return mixed|null + */ + protected function getData($name) + { + if (array_key_exists($name, $this->data)) { + return $this->data[$name]; + } + + $data = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/thread10:' . $name . ')'); + + if (!$data) { + $data = null; + } + + $this->data[$name] = $data; + + return $data; + } + + /** + * Register Atom Thread Extension 1.0 namespace + * + * @return void + */ + protected function registerNamespaces() + { + $this->xpath->registerNamespace('thread10', 'http://purl.org/syndication/thread/1.0'); + } +} diff --git a/library/Zend/Feed/Reader/Extension/WellFormedWeb/Entry.php b/library/Zend/Feed/Reader/Extension/WellFormedWeb/Entry.php new file mode 100755 index 0000000000..6d5a977053 --- /dev/null +++ b/library/Zend/Feed/Reader/Extension/WellFormedWeb/Entry.php @@ -0,0 +1,50 @@ +data)) { + return $this->data[$name]; + } + + $data = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/wfw:' . $name . ')'); + + if (!$data) { + $data = null; + } + + $this->data[$name] = $data; + + return $data; + } + + /** + * Register Slash namespaces + * + * @return void + */ + protected function registerNamespaces() + { + $this->xpath->registerNamespace('wfw', 'http://wellformedweb.org/CommentAPI/'); + } +} diff --git a/library/Zend/Feed/Reader/ExtensionManager.php b/library/Zend/Feed/Reader/ExtensionManager.php new file mode 100755 index 0000000000..9103643a30 --- /dev/null +++ b/library/Zend/Feed/Reader/ExtensionManager.php @@ -0,0 +1,80 @@ +pluginManager = $pluginManager; + } + + /** + * Method overloading + * + * Proxy to composed ExtensionPluginManager instance. + * + * @param string $method + * @param array $args + * @return mixed + * @throws Exception\BadMethodCallException + */ + public function __call($method, $args) + { + if (!method_exists($this->pluginManager, $method)) { + throw new Exception\BadMethodCallException(sprintf( + 'Method by name of %s does not exist in %s', + $method, + __CLASS__ + )); + } + return call_user_func_array(array($this->pluginManager, $method), $args); + } + + /** + * Get the named extension + * + * @param string $name + * @return Extension\AbstractEntry|Extension\AbstractFeed + */ + public function get($name) + { + return $this->pluginManager->get($name); + } + + /** + * Do we have the named extension? + * + * @param string $name + * @return bool + */ + public function has($name) + { + return $this->pluginManager->has($name); + } +} diff --git a/library/Zend/Feed/Reader/ExtensionManagerInterface.php b/library/Zend/Feed/Reader/ExtensionManagerInterface.php new file mode 100755 index 0000000000..4bbb91d9e9 --- /dev/null +++ b/library/Zend/Feed/Reader/ExtensionManagerInterface.php @@ -0,0 +1,29 @@ + 'Zend\Feed\Reader\Extension\Atom\Entry', + 'atomfeed' => 'Zend\Feed\Reader\Extension\Atom\Feed', + 'contententry' => 'Zend\Feed\Reader\Extension\Content\Entry', + 'creativecommonsentry' => 'Zend\Feed\Reader\Extension\CreativeCommons\Entry', + 'creativecommonsfeed' => 'Zend\Feed\Reader\Extension\CreativeCommons\Feed', + 'dublincoreentry' => 'Zend\Feed\Reader\Extension\DublinCore\Entry', + 'dublincorefeed' => 'Zend\Feed\Reader\Extension\DublinCore\Feed', + 'podcastentry' => 'Zend\Feed\Reader\Extension\Podcast\Entry', + 'podcastfeed' => 'Zend\Feed\Reader\Extension\Podcast\Feed', + 'slashentry' => 'Zend\Feed\Reader\Extension\Slash\Entry', + 'syndicationfeed' => 'Zend\Feed\Reader\Extension\Syndication\Feed', + 'threadentry' => 'Zend\Feed\Reader\Extension\Thread\Entry', + 'wellformedwebentry' => 'Zend\Feed\Reader\Extension\WellFormedWeb\Entry', + ); + + /** + * Do not share instances + * + * @var bool + */ + protected $shareByDefault = false; + + /** + * Validate the plugin + * + * Checks that the extension loaded is of a valid type. + * + * @param mixed $plugin + * @return void + * @throws Exception\InvalidArgumentException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof Extension\AbstractEntry + || $plugin instanceof Extension\AbstractFeed + ) { + // we're okay + return; + } + + throw new Exception\InvalidArgumentException(sprintf( + 'Plugin of type %s is invalid; must implement %s\Extension\AbstractFeed ' + . 'or %s\Extension\AbstractEntry', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__, + __NAMESPACE__ + )); + } +} diff --git a/library/Zend/Feed/Reader/Feed/AbstractFeed.php b/library/Zend/Feed/Reader/Feed/AbstractFeed.php new file mode 100755 index 0000000000..dd616bef7a --- /dev/null +++ b/library/Zend/Feed/Reader/Feed/AbstractFeed.php @@ -0,0 +1,307 @@ +domDocument = $domDocument; + $this->xpath = new DOMXPath($this->domDocument); + + if ($type !== null) { + $this->data['type'] = $type; + } else { + $this->data['type'] = Reader\Reader::detectType($this->domDocument); + } + $this->registerNamespaces(); + $this->indexEntries(); + $this->loadExtensions(); + } + + /** + * Set an original source URI for the feed being parsed. This value + * is returned from getFeedLink() method if the feed does not carry + * a self-referencing URI. + * + * @param string $uri + */ + public function setOriginalSourceUri($uri) + { + $this->originalSourceUri = $uri; + } + + /** + * Get an original source URI for the feed being parsed. Returns null if + * unset or the feed was not imported from a URI. + * + * @return string|null + */ + public function getOriginalSourceUri() + { + return $this->originalSourceUri; + } + + /** + * Get the number of feed entries. + * Required by the Iterator interface. + * + * @return int + */ + public function count() + { + return count($this->entries); + } + + /** + * Return the current entry + * + * @return \Zend\Feed\Reader\Entry\EntryInterface + */ + public function current() + { + if (substr($this->getType(), 0, 3) == 'rss') { + $reader = new Reader\Entry\Rss($this->entries[$this->key()], $this->key(), $this->getType()); + } else { + $reader = new Reader\Entry\Atom($this->entries[$this->key()], $this->key(), $this->getType()); + } + + $reader->setXpath($this->xpath); + + return $reader; + } + + /** + * Get the DOM + * + * @return DOMDocument + */ + public function getDomDocument() + { + return $this->domDocument; + } + + /** + * Get the Feed's encoding + * + * @return string + */ + public function getEncoding() + { + $assumed = $this->getDomDocument()->encoding; + if (empty($assumed)) { + $assumed = 'UTF-8'; + } + return $assumed; + } + + /** + * Get feed as xml + * + * @return string + */ + public function saveXml() + { + return $this->getDomDocument()->saveXml(); + } + + /** + * Get the DOMElement representing the items/feed element + * + * @return DOMElement + */ + public function getElement() + { + return $this->getDomDocument()->documentElement; + } + + /** + * Get the DOMXPath object for this feed + * + * @return DOMXPath + */ + public function getXpath() + { + return $this->xpath; + } + + /** + * Get the feed type + * + * @return string + */ + public function getType() + { + return $this->data['type']; + } + + /** + * Return the current feed key + * + * @return int + */ + public function key() + { + return $this->entriesKey; + } + + /** + * Move the feed pointer forward + * + */ + public function next() + { + ++$this->entriesKey; + } + + /** + * Reset the pointer in the feed object + * + */ + public function rewind() + { + $this->entriesKey = 0; + } + + /** + * Check to see if the iterator is still valid + * + * @return bool + */ + public function valid() + { + return 0 <= $this->entriesKey && $this->entriesKey < $this->count(); + } + + public function getExtensions() + { + return $this->extensions; + } + + public function __call($method, $args) + { + foreach ($this->extensions as $extension) { + if (method_exists($extension, $method)) { + return call_user_func_array(array($extension, $method), $args); + } + } + throw new Exception\BadMethodCallException('Method: ' . $method + . 'does not exist and could not be located on a registered Extension'); + } + + /** + * Return an Extension object with the matching name (postfixed with _Feed) + * + * @param string $name + * @return \Zend\Feed\Reader\Extension\AbstractFeed + */ + public function getExtension($name) + { + if (array_key_exists($name . '\\Feed', $this->extensions)) { + return $this->extensions[$name . '\\Feed']; + } + return null; + } + + protected function loadExtensions() + { + $all = Reader\Reader::getExtensions(); + $manager = Reader\Reader::getExtensionManager(); + $feed = $all['feed']; + foreach ($feed as $extension) { + if (in_array($extension, $all['core'])) { + continue; + } + if (!$manager->has($extension)) { + throw new Exception\RuntimeException(sprintf('Unable to load extension "%s"; cannot find class', $extension)); + } + $plugin = $manager->get($extension); + $plugin->setDomDocument($this->getDomDocument()); + $plugin->setType($this->data['type']); + $plugin->setXpath($this->xpath); + $this->extensions[$extension] = $plugin; + } + } + + /** + * Read all entries to the internal entries array + * + */ + abstract protected function indexEntries(); + + /** + * Register the default namespaces for the current feed format + * + */ + abstract protected function registerNamespaces(); +} diff --git a/library/Zend/Feed/Reader/Feed/Atom.php b/library/Zend/Feed/Reader/Feed/Atom.php new file mode 100755 index 0000000000..72efcf7609 --- /dev/null +++ b/library/Zend/Feed/Reader/Feed/Atom.php @@ -0,0 +1,408 @@ +get('Atom\Feed'); + $atomFeed->setDomDocument($dom); + $atomFeed->setType($this->data['type']); + $atomFeed->setXpath($this->xpath); + $this->extensions['Atom\\Feed'] = $atomFeed; + + $atomFeed = $manager->get('DublinCore\Feed'); + $atomFeed->setDomDocument($dom); + $atomFeed->setType($this->data['type']); + $atomFeed->setXpath($this->xpath); + $this->extensions['DublinCore\\Feed'] = $atomFeed; + + foreach ($this->extensions as $extension) { + $extension->setXpathPrefix('/atom:feed'); + } + } + + /** + * Get a single author + * + * @param int $index + * @return string|null + */ + public function getAuthor($index = 0) + { + $authors = $this->getAuthors(); + + if (isset($authors[$index])) { + return $authors[$index]; + } + + return null; + } + + /** + * Get an array with feed authors + * + * @return array + */ + public function getAuthors() + { + if (array_key_exists('authors', $this->data)) { + return $this->data['authors']; + } + + $authors = $this->getExtension('Atom')->getAuthors(); + + $this->data['authors'] = $authors; + + return $this->data['authors']; + } + + /** + * Get the copyright entry + * + * @return string|null + */ + public function getCopyright() + { + if (array_key_exists('copyright', $this->data)) { + return $this->data['copyright']; + } + + $copyright = $this->getExtension('Atom')->getCopyright(); + + if (!$copyright) { + $copyright = null; + } + + $this->data['copyright'] = $copyright; + + return $this->data['copyright']; + } + + /** + * Get the feed creation date + * + * @return string|null + */ + public function getDateCreated() + { + if (array_key_exists('datecreated', $this->data)) { + return $this->data['datecreated']; + } + + $dateCreated = $this->getExtension('Atom')->getDateCreated(); + + if (!$dateCreated) { + $dateCreated = null; + } + + $this->data['datecreated'] = $dateCreated; + + return $this->data['datecreated']; + } + + /** + * Get the feed modification date + * + * @return string|null + */ + public function getDateModified() + { + if (array_key_exists('datemodified', $this->data)) { + return $this->data['datemodified']; + } + + $dateModified = $this->getExtension('Atom')->getDateModified(); + + if (!$dateModified) { + $dateModified = null; + } + + $this->data['datemodified'] = $dateModified; + + return $this->data['datemodified']; + } + + /** + * Get the feed lastBuild date. This is not implemented in Atom. + * + * @return string|null + */ + public function getLastBuildDate() + { + return null; + } + + /** + * Get the feed description + * + * @return string|null + */ + public function getDescription() + { + if (array_key_exists('description', $this->data)) { + return $this->data['description']; + } + + $description = $this->getExtension('Atom')->getDescription(); + + if (!$description) { + $description = null; + } + + $this->data['description'] = $description; + + return $this->data['description']; + } + + /** + * Get the feed generator entry + * + * @return string|null + */ + public function getGenerator() + { + if (array_key_exists('generator', $this->data)) { + return $this->data['generator']; + } + + $generator = $this->getExtension('Atom')->getGenerator(); + + $this->data['generator'] = $generator; + + return $this->data['generator']; + } + + /** + * Get the feed ID + * + * @return string|null + */ + public function getId() + { + if (array_key_exists('id', $this->data)) { + return $this->data['id']; + } + + $id = $this->getExtension('Atom')->getId(); + + $this->data['id'] = $id; + + return $this->data['id']; + } + + /** + * Get the feed language + * + * @return string|null + */ + public function getLanguage() + { + if (array_key_exists('language', $this->data)) { + return $this->data['language']; + } + + $language = $this->getExtension('Atom')->getLanguage(); + + if (!$language) { + $language = $this->xpath->evaluate('string(//@xml:lang[1])'); + } + + if (!$language) { + $language = null; + } + + $this->data['language'] = $language; + + return $this->data['language']; + } + + /** + * Get a link to the source website + * + * @return string|null + */ + public function getBaseUrl() + { + if (array_key_exists('baseUrl', $this->data)) { + return $this->data['baseUrl']; + } + + $baseUrl = $this->getExtension('Atom')->getBaseUrl(); + + $this->data['baseUrl'] = $baseUrl; + + return $this->data['baseUrl']; + } + + /** + * Get a link to the source website + * + * @return string|null + */ + public function getLink() + { + if (array_key_exists('link', $this->data)) { + return $this->data['link']; + } + + $link = $this->getExtension('Atom')->getLink(); + + $this->data['link'] = $link; + + return $this->data['link']; + } + + /** + * Get feed image data + * + * @return array|null + */ + public function getImage() + { + if (array_key_exists('image', $this->data)) { + return $this->data['image']; + } + + $link = $this->getExtension('Atom')->getImage(); + + $this->data['image'] = $link; + + return $this->data['image']; + } + + /** + * Get a link to the feed's XML Url + * + * @return string|null + */ + public function getFeedLink() + { + if (array_key_exists('feedlink', $this->data)) { + return $this->data['feedlink']; + } + + $link = $this->getExtension('Atom')->getFeedLink(); + + if ($link === null || empty($link)) { + $link = $this->getOriginalSourceUri(); + } + + $this->data['feedlink'] = $link; + + return $this->data['feedlink']; + } + + /** + * Get the feed title + * + * @return string|null + */ + public function getTitle() + { + if (array_key_exists('title', $this->data)) { + return $this->data['title']; + } + + $title = $this->getExtension('Atom')->getTitle(); + + $this->data['title'] = $title; + + return $this->data['title']; + } + + /** + * Get an array of any supported Pusubhubbub endpoints + * + * @return array|null + */ + public function getHubs() + { + if (array_key_exists('hubs', $this->data)) { + return $this->data['hubs']; + } + + $hubs = $this->getExtension('Atom')->getHubs(); + + $this->data['hubs'] = $hubs; + + return $this->data['hubs']; + } + + /** + * Get all categories + * + * @return Reader\Collection\Category + */ + public function getCategories() + { + if (array_key_exists('categories', $this->data)) { + return $this->data['categories']; + } + + $categoryCollection = $this->getExtension('Atom')->getCategories(); + + if (count($categoryCollection) == 0) { + $categoryCollection = $this->getExtension('DublinCore')->getCategories(); + } + + $this->data['categories'] = $categoryCollection; + + return $this->data['categories']; + } + + /** + * Read all entries to the internal entries array + * + * @return void + */ + protected function indexEntries() + { + if ($this->getType() == Reader\Reader::TYPE_ATOM_10 || + $this->getType() == Reader\Reader::TYPE_ATOM_03) { + $entries = $this->xpath->evaluate('//atom:entry'); + + foreach ($entries as $index => $entry) { + $this->entries[$index] = $entry; + } + } + } + + /** + * Register the default namespaces for the current feed format + * + */ + protected function registerNamespaces() + { + switch ($this->data['type']) { + case Reader\Reader::TYPE_ATOM_03: + $this->xpath->registerNamespace('atom', Reader\Reader::NAMESPACE_ATOM_03); + break; + case Reader\Reader::TYPE_ATOM_10: + default: + $this->xpath->registerNamespace('atom', Reader\Reader::NAMESPACE_ATOM_10); + } + } +} diff --git a/library/Zend/Feed/Reader/Feed/Atom/Source.php b/library/Zend/Feed/Reader/Feed/Atom/Source.php new file mode 100755 index 0000000000..5eabd974bd --- /dev/null +++ b/library/Zend/Feed/Reader/Feed/Atom/Source.php @@ -0,0 +1,107 @@ +domDocument = $source->ownerDocument; + $this->xpath = new DOMXPath($this->domDocument); + $this->data['type'] = $type; + $this->registerNamespaces(); + $this->loadExtensions(); + + $manager = Reader\Reader::getExtensionManager(); + $extensions = array('Atom\Feed', 'DublinCore\Feed'); + + foreach ($extensions as $name) { + $extension = $manager->get($name); + $extension->setDomDocument($this->domDocument); + $extension->setType($this->data['type']); + $extension->setXpath($this->xpath); + $this->extensions[$name] = $extension; + } + + foreach ($this->extensions as $extension) { + $extension->setXpathPrefix(rtrim($xpathPrefix, '/') . '/atom:source'); + } + } + + /** + * Since this is not an Entry carrier but a vehicle for Feed metadata, any + * applicable Entry methods are stubbed out and do nothing. + */ + + /** + * @return void + */ + public function count() + { + } + + /** + * @return void + */ + public function current() + { + } + + /** + * @return void + */ + public function key() + { + } + + /** + * @return void + */ + public function next() + { + } + + /** + * @return void + */ + public function rewind() + { + } + + /** + * @return void + */ + public function valid() + { + } + + /** + * @return void + */ + protected function indexEntries() + { + } +} diff --git a/library/Zend/Feed/Reader/Feed/FeedInterface.php b/library/Zend/Feed/Reader/Feed/FeedInterface.php new file mode 100755 index 0000000000..c98a1b333b --- /dev/null +++ b/library/Zend/Feed/Reader/Feed/FeedInterface.php @@ -0,0 +1,110 @@ +get('DublinCore\Feed'); + $feed->setDomDocument($dom); + $feed->setType($this->data['type']); + $feed->setXpath($this->xpath); + $this->extensions['DublinCore\Feed'] = $feed; + + $feed = $manager->get('Atom\Feed'); + $feed->setDomDocument($dom); + $feed->setType($this->data['type']); + $feed->setXpath($this->xpath); + $this->extensions['Atom\Feed'] = $feed; + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 + && $this->getType() !== Reader\Reader::TYPE_RSS_090 + ) { + $xpathPrefix = '/rss/channel'; + } else { + $xpathPrefix = '/rdf:RDF/rss:channel'; + } + foreach ($this->extensions as $extension) { + $extension->setXpathPrefix($xpathPrefix); + } + } + + /** + * Get a single author + * + * @param int $index + * @return string|null + */ + public function getAuthor($index = 0) + { + $authors = $this->getAuthors(); + + if (isset($authors[$index])) { + return $authors[$index]; + } + + return null; + } + + /** + * Get an array with feed authors + * + * @return array + */ + public function getAuthors() + { + if (array_key_exists('authors', $this->data)) { + return $this->data['authors']; + } + + $authors = array(); + $authorsDc = $this->getExtension('DublinCore')->getAuthors(); + if (!empty($authorsDc)) { + foreach ($authorsDc as $author) { + $authors[] = array( + 'name' => $author['name'] + ); + } + } + + /** + * Technically RSS doesn't specific author element use at the feed level + * but it's supported on a "just in case" basis. + */ + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 + && $this->getType() !== Reader\Reader::TYPE_RSS_090) { + $list = $this->xpath->query('//author'); + } else { + $list = $this->xpath->query('//rss:author'); + } + if ($list->length) { + foreach ($list as $author) { + $string = trim($author->nodeValue); + $email = null; + $name = null; + $data = array(); + // Pretty rough parsing - but it's a catchall + if (preg_match("/^.*@[^ ]*/", $string, $matches)) { + $data['email'] = trim($matches[0]); + if (preg_match("/\((.*)\)$/", $string, $matches)) { + $data['name'] = $matches[1]; + } + $authors[] = $data; + } + } + } + + if (count($authors) == 0) { + $authors = $this->getExtension('Atom')->getAuthors(); + } else { + $authors = new Reader\Collection\Author( + Reader\Reader::arrayUnique($authors) + ); + } + + if (count($authors) == 0) { + $authors = null; + } + + $this->data['authors'] = $authors; + + return $this->data['authors']; + } + + /** + * Get the copyright entry + * + * @return string|null + */ + public function getCopyright() + { + if (array_key_exists('copyright', $this->data)) { + return $this->data['copyright']; + } + + $copyright = null; + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 && + $this->getType() !== Reader\Reader::TYPE_RSS_090) { + $copyright = $this->xpath->evaluate('string(/rss/channel/copyright)'); + } + + if (!$copyright && $this->getExtension('DublinCore') !== null) { + $copyright = $this->getExtension('DublinCore')->getCopyright(); + } + + if (empty($copyright)) { + $copyright = $this->getExtension('Atom')->getCopyright(); + } + + if (!$copyright) { + $copyright = null; + } + + $this->data['copyright'] = $copyright; + + return $this->data['copyright']; + } + + /** + * Get the feed creation date + * + * @return string|null + */ + public function getDateCreated() + { + return $this->getDateModified(); + } + + /** + * Get the feed modification date + * + * @return DateTime + * @throws Exception\RuntimeException + */ + public function getDateModified() + { + if (array_key_exists('datemodified', $this->data)) { + return $this->data['datemodified']; + } + + $dateModified = null; + $date = null; + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 && + $this->getType() !== Reader\Reader::TYPE_RSS_090) { + $dateModified = $this->xpath->evaluate('string(/rss/channel/pubDate)'); + if (!$dateModified) { + $dateModified = $this->xpath->evaluate('string(/rss/channel/lastBuildDate)'); + } + if ($dateModified) { + $dateModifiedParsed = strtotime($dateModified); + if ($dateModifiedParsed) { + $date = new DateTime('@' . $dateModifiedParsed); + } else { + $dateStandards = array(DateTime::RSS, DateTime::RFC822, + DateTime::RFC2822, null); + foreach ($dateStandards as $standard) { + try { + $date = DateTime::createFromFormat($standard, $dateModified); + break; + } catch (\Exception $e) { + if ($standard == null) { + throw new Exception\RuntimeException( + 'Could not load date due to unrecognised' + .' format (should follow RFC 822 or 2822):' + . $e->getMessage(), + 0, $e + ); + } + } + } + } + } + } + + if (!$date) { + $date = $this->getExtension('DublinCore')->getDate(); + } + + if (!$date) { + $date = $this->getExtension('Atom')->getDateModified(); + } + + if (!$date) { + $date = null; + } + + $this->data['datemodified'] = $date; + + return $this->data['datemodified']; + } + + /** + * Get the feed lastBuild date + * + * @throws Exception\RuntimeException + * @return DateTime + */ + public function getLastBuildDate() + { + if (array_key_exists('lastBuildDate', $this->data)) { + return $this->data['lastBuildDate']; + } + + $lastBuildDate = null; + $date = null; + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 && + $this->getType() !== Reader\Reader::TYPE_RSS_090) { + $lastBuildDate = $this->xpath->evaluate('string(/rss/channel/lastBuildDate)'); + if ($lastBuildDate) { + $lastBuildDateParsed = strtotime($lastBuildDate); + if ($lastBuildDateParsed) { + $date = new DateTime('@' . $lastBuildDateParsed); + } else { + $dateStandards = array(DateTime::RSS, DateTime::RFC822, + DateTime::RFC2822, null); + foreach ($dateStandards as $standard) { + try { + $date = DateTime::createFromFormat($standard, $lastBuildDateParsed); + break; + } catch (\Exception $e) { + if ($standard == null) { + throw new Exception\RuntimeException( + 'Could not load date due to unrecognised' + .' format (should follow RFC 822 or 2822):' + . $e->getMessage(), + 0, $e + ); + } + } + } + } + } + } + + if (!$date) { + $date = null; + } + + $this->data['lastBuildDate'] = $date; + + return $this->data['lastBuildDate']; + } + + /** + * Get the feed description + * + * @return string|null + */ + public function getDescription() + { + if (array_key_exists('description', $this->data)) { + return $this->data['description']; + } + + $description = null; + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 && + $this->getType() !== Reader\Reader::TYPE_RSS_090) { + $description = $this->xpath->evaluate('string(/rss/channel/description)'); + } else { + $description = $this->xpath->evaluate('string(/rdf:RDF/rss:channel/rss:description)'); + } + + if (!$description && $this->getExtension('DublinCore') !== null) { + $description = $this->getExtension('DublinCore')->getDescription(); + } + + if (empty($description)) { + $description = $this->getExtension('Atom')->getDescription(); + } + + if (!$description) { + $description = null; + } + + $this->data['description'] = $description; + + return $this->data['description']; + } + + /** + * Get the feed ID + * + * @return string|null + */ + public function getId() + { + if (array_key_exists('id', $this->data)) { + return $this->data['id']; + } + + $id = null; + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 && + $this->getType() !== Reader\Reader::TYPE_RSS_090) { + $id = $this->xpath->evaluate('string(/rss/channel/guid)'); + } + + if (!$id && $this->getExtension('DublinCore') !== null) { + $id = $this->getExtension('DublinCore')->getId(); + } + + if (empty($id)) { + $id = $this->getExtension('Atom')->getId(); + } + + if (!$id) { + if ($this->getLink()) { + $id = $this->getLink(); + } elseif ($this->getTitle()) { + $id = $this->getTitle(); + } else { + $id = null; + } + } + + $this->data['id'] = $id; + + return $this->data['id']; + } + + /** + * Get the feed image data + * + * @return array|null + */ + public function getImage() + { + if (array_key_exists('image', $this->data)) { + return $this->data['image']; + } + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 && + $this->getType() !== Reader\Reader::TYPE_RSS_090) { + $list = $this->xpath->query('/rss/channel/image'); + $prefix = '/rss/channel/image[1]'; + } else { + $list = $this->xpath->query('/rdf:RDF/rss:channel/rss:image'); + $prefix = '/rdf:RDF/rss:channel/rss:image[1]'; + } + if ($list->length > 0) { + $image = array(); + $value = $this->xpath->evaluate('string(' . $prefix . '/url)'); + if ($value) { + $image['uri'] = $value; + } + $value = $this->xpath->evaluate('string(' . $prefix . '/link)'); + if ($value) { + $image['link'] = $value; + } + $value = $this->xpath->evaluate('string(' . $prefix . '/title)'); + if ($value) { + $image['title'] = $value; + } + $value = $this->xpath->evaluate('string(' . $prefix . '/height)'); + if ($value) { + $image['height'] = $value; + } + $value = $this->xpath->evaluate('string(' . $prefix . '/width)'); + if ($value) { + $image['width'] = $value; + } + $value = $this->xpath->evaluate('string(' . $prefix . '/description)'); + if ($value) { + $image['description'] = $value; + } + } else { + $image = null; + } + + $this->data['image'] = $image; + + return $this->data['image']; + } + + /** + * Get the feed language + * + * @return string|null + */ + public function getLanguage() + { + if (array_key_exists('language', $this->data)) { + return $this->data['language']; + } + + $language = null; + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 && + $this->getType() !== Reader\Reader::TYPE_RSS_090) { + $language = $this->xpath->evaluate('string(/rss/channel/language)'); + } + + if (!$language && $this->getExtension('DublinCore') !== null) { + $language = $this->getExtension('DublinCore')->getLanguage(); + } + + if (empty($language)) { + $language = $this->getExtension('Atom')->getLanguage(); + } + + if (!$language) { + $language = $this->xpath->evaluate('string(//@xml:lang[1])'); + } + + if (!$language) { + $language = null; + } + + $this->data['language'] = $language; + + return $this->data['language']; + } + + /** + * Get a link to the feed + * + * @return string|null + */ + public function getLink() + { + if (array_key_exists('link', $this->data)) { + return $this->data['link']; + } + + $link = null; + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 && + $this->getType() !== Reader\Reader::TYPE_RSS_090) { + $link = $this->xpath->evaluate('string(/rss/channel/link)'); + } else { + $link = $this->xpath->evaluate('string(/rdf:RDF/rss:channel/rss:link)'); + } + + if (empty($link)) { + $link = $this->getExtension('Atom')->getLink(); + } + + if (!$link) { + $link = null; + } + + $this->data['link'] = $link; + + return $this->data['link']; + } + + /** + * Get a link to the feed XML + * + * @return string|null + */ + public function getFeedLink() + { + if (array_key_exists('feedlink', $this->data)) { + return $this->data['feedlink']; + } + + $link = null; + + $link = $this->getExtension('Atom')->getFeedLink(); + + if ($link === null || empty($link)) { + $link = $this->getOriginalSourceUri(); + } + + $this->data['feedlink'] = $link; + + return $this->data['feedlink']; + } + + /** + * Get the feed generator entry + * + * @return string|null + */ + public function getGenerator() + { + if (array_key_exists('generator', $this->data)) { + return $this->data['generator']; + } + + $generator = null; + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 && + $this->getType() !== Reader\Reader::TYPE_RSS_090) { + $generator = $this->xpath->evaluate('string(/rss/channel/generator)'); + } + + if (!$generator) { + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 && + $this->getType() !== Reader\Reader::TYPE_RSS_090) { + $generator = $this->xpath->evaluate('string(/rss/channel/atom:generator)'); + } else { + $generator = $this->xpath->evaluate('string(/rdf:RDF/rss:channel/atom:generator)'); + } + } + + if (empty($generator)) { + $generator = $this->getExtension('Atom')->getGenerator(); + } + + if (!$generator) { + $generator = null; + } + + $this->data['generator'] = $generator; + + return $this->data['generator']; + } + + /** + * Get the feed title + * + * @return string|null + */ + public function getTitle() + { + if (array_key_exists('title', $this->data)) { + return $this->data['title']; + } + + $title = null; + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 && + $this->getType() !== Reader\Reader::TYPE_RSS_090) { + $title = $this->xpath->evaluate('string(/rss/channel/title)'); + } else { + $title = $this->xpath->evaluate('string(/rdf:RDF/rss:channel/rss:title)'); + } + + if (!$title && $this->getExtension('DublinCore') !== null) { + $title = $this->getExtension('DublinCore')->getTitle(); + } + + if (!$title) { + $title = $this->getExtension('Atom')->getTitle(); + } + + if (!$title) { + $title = null; + } + + $this->data['title'] = $title; + + return $this->data['title']; + } + + /** + * Get an array of any supported Pusubhubbub endpoints + * + * @return array|null + */ + public function getHubs() + { + if (array_key_exists('hubs', $this->data)) { + return $this->data['hubs']; + } + + $hubs = $this->getExtension('Atom')->getHubs(); + + if (empty($hubs)) { + $hubs = null; + } else { + $hubs = array_unique($hubs); + } + + $this->data['hubs'] = $hubs; + + return $this->data['hubs']; + } + + /** + * Get all categories + * + * @return Reader\Collection\Category + */ + public function getCategories() + { + if (array_key_exists('categories', $this->data)) { + return $this->data['categories']; + } + + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 && + $this->getType() !== Reader\Reader::TYPE_RSS_090) { + $list = $this->xpath->query('/rss/channel//category'); + } else { + $list = $this->xpath->query('/rdf:RDF/rss:channel//rss:category'); + } + + if ($list->length) { + $categoryCollection = new Collection\Category; + foreach ($list as $category) { + $categoryCollection[] = array( + 'term' => $category->nodeValue, + 'scheme' => $category->getAttribute('domain'), + 'label' => $category->nodeValue, + ); + } + } else { + $categoryCollection = $this->getExtension('DublinCore')->getCategories(); + } + + if (count($categoryCollection) == 0) { + $categoryCollection = $this->getExtension('Atom')->getCategories(); + } + + $this->data['categories'] = $categoryCollection; + + return $this->data['categories']; + } + + /** + * Read all entries to the internal entries array + * + */ + protected function indexEntries() + { + if ($this->getType() !== Reader\Reader::TYPE_RSS_10 && $this->getType() !== Reader\Reader::TYPE_RSS_090) { + $entries = $this->xpath->evaluate('//item'); + } else { + $entries = $this->xpath->evaluate('//rss:item'); + } + + foreach ($entries as $index => $entry) { + $this->entries[$index] = $entry; + } + } + + /** + * Register the default namespaces for the current feed format + * + */ + protected function registerNamespaces() + { + switch ($this->data['type']) { + case Reader\Reader::TYPE_RSS_10: + $this->xpath->registerNamespace('rdf', Reader\Reader::NAMESPACE_RDF); + $this->xpath->registerNamespace('rss', Reader\Reader::NAMESPACE_RSS_10); + break; + + case Reader\Reader::TYPE_RSS_090: + $this->xpath->registerNamespace('rdf', Reader\Reader::NAMESPACE_RDF); + $this->xpath->registerNamespace('rss', Reader\Reader::NAMESPACE_RSS_090); + break; + } + } +} diff --git a/library/Zend/Feed/Reader/FeedSet.php b/library/Zend/Feed/Reader/FeedSet.php new file mode 100755 index 0000000000..e487bc8f4c --- /dev/null +++ b/library/Zend/Feed/Reader/FeedSet.php @@ -0,0 +1,126 @@ +getAttribute('rel')) !== 'alternate' + || !$link->getAttribute('type') || !$link->getAttribute('href')) { + continue; + } + if (!isset($this->rss) && $link->getAttribute('type') == 'application/rss+xml') { + $this->rss = $this->absolutiseUri(trim($link->getAttribute('href')), $uri); + } elseif (!isset($this->atom) && $link->getAttribute('type') == 'application/atom+xml') { + $this->atom = $this->absolutiseUri(trim($link->getAttribute('href')), $uri); + } elseif (!isset($this->rdf) && $link->getAttribute('type') == 'application/rdf+xml') { + $this->rdf = $this->absolutiseUri(trim($link->getAttribute('href')), $uri); + } + $this[] = new static(array( + 'rel' => 'alternate', + 'type' => $link->getAttribute('type'), + 'href' => $this->absolutiseUri(trim($link->getAttribute('href')), $uri), + )); + } + } + + /** + * Attempt to turn a relative URI into an absolute URI + */ + protected function absolutiseUri($link, $uri = null) + { + $linkUri = Uri::factory($link); + if (!$linkUri->isAbsolute() or !$linkUri->isValid()) { + if ($uri !== null) { + $uri = Uri::factory($uri); + + if ($link[0] !== '/') { + $link = $uri->getPath() . '/' . $link; + } + + $link = $uri->getScheme() . '://' . $uri->getHost() . '/' . $this->canonicalizePath($link); + if (!Uri::factory($link)->isValid()) { + $link = null; + } + } + } + return $link; + } + + /** + * Canonicalize relative path + */ + protected function canonicalizePath($path) + { + $parts = array_filter(explode('/', $path)); + $absolutes = array(); + foreach ($parts as $part) { + if ('.' == $part) { + continue; + } + if ('..' == $part) { + array_pop($absolutes); + } else { + $absolutes[] = $part; + } + } + return implode('/', $absolutes); + } + + /** + * Supports lazy loading of feeds using Reader::import() but + * delegates any other operations to the parent class. + * + * @param string $offset + * @return mixed + */ + public function offsetGet($offset) + { + if ($offset == 'feed' && !$this->offsetExists('feed')) { + if (!$this->offsetExists('href')) { + return null; + } + $feed = Reader::import($this->offsetGet('href')); + $this->offsetSet('feed', $feed); + return $feed; + } + return parent::offsetGet($offset); + } +} diff --git a/library/Zend/Feed/Reader/Http/ClientInterface.php b/library/Zend/Feed/Reader/Http/ClientInterface.php new file mode 100755 index 0000000000..43932f7612 --- /dev/null +++ b/library/Zend/Feed/Reader/Http/ClientInterface.php @@ -0,0 +1,21 @@ + array( + 'DublinCore\Feed', + 'Atom\Feed' + ), + 'entry' => array( + 'Content\Entry', + 'DublinCore\Entry', + 'Atom\Entry' + ), + 'core' => array( + 'DublinCore\Feed', + 'Atom\Feed', + 'Content\Entry', + 'DublinCore\Entry', + 'Atom\Entry' + ) + ); + + /** + * Get the Feed cache + * + * @return CacheStorage + */ + public static function getCache() + { + return static::$cache; + } + + /** + * Set the feed cache + * + * @param CacheStorage $cache + * @return void + */ + public static function setCache(CacheStorage $cache) + { + static::$cache = $cache; + } + + /** + * Set the HTTP client instance + * + * Sets the HTTP client object to use for retrieving the feeds. + * + * @param ZendHttp\Client $httpClient + * @return void + */ + public static function setHttpClient(ZendHttp\Client $httpClient) + { + static::$httpClient = $httpClient; + } + + + /** + * Gets the HTTP client object. If none is set, a new ZendHttp\Client will be used. + * + * @return ZendHttp\Client + */ + public static function getHttpClient() + { + if (!static::$httpClient instanceof ZendHttp\Client) { + static::$httpClient = new ZendHttp\Client(); + } + + return static::$httpClient; + } + + /** + * Toggle using POST instead of PUT and DELETE HTTP methods + * + * Some feed implementations do not accept PUT and DELETE HTTP + * methods, or they can't be used because of proxies or other + * measures. This allows turning on using POST where PUT and + * DELETE would normally be used; in addition, an + * X-Method-Override header will be sent with a value of PUT or + * DELETE as appropriate. + * + * @param bool $override Whether to override PUT and DELETE. + * @return void + */ + public static function setHttpMethodOverride($override = true) + { + static::$httpMethodOverride = $override; + } + + /** + * Get the HTTP override state + * + * @return bool + */ + public static function getHttpMethodOverride() + { + return static::$httpMethodOverride; + } + + /** + * Set the flag indicating whether or not to use HTTP conditional GET + * + * @param bool $bool + * @return void + */ + public static function useHttpConditionalGet($bool = true) + { + static::$httpConditionalGet = $bool; + } + + /** + * Import a feed by providing a URI + * + * @param string $uri The URI to the feed + * @param string $etag OPTIONAL Last received ETag for this resource + * @param string $lastModified OPTIONAL Last-Modified value for this resource + * @return Feed\FeedInterface + * @throws Exception\RuntimeException + */ + public static function import($uri, $etag = null, $lastModified = null) + { + $cache = self::getCache(); + $feed = null; + $client = self::getHttpClient(); + $client->resetParameters(); + $headers = new ZendHttp\Headers(); + $client->setHeaders($headers); + $client->setUri($uri); + $cacheId = 'Zend_Feed_Reader_' . md5($uri); + + if (static::$httpConditionalGet && $cache) { + $data = $cache->getItem($cacheId); + if ($data) { + if ($etag === null) { + $etag = $cache->getItem($cacheId . '_etag'); + } + if ($lastModified === null) { + $lastModified = $cache->getItem($cacheId . '_lastmodified'); + } + if ($etag) { + $headers->addHeaderLine('If-None-Match', $etag); + } + if ($lastModified) { + $headers->addHeaderLine('If-Modified-Since', $lastModified); + } + } + $response = $client->send(); + if ($response->getStatusCode() !== 200 && $response->getStatusCode() !== 304) { + throw new Exception\RuntimeException('Feed failed to load, got response code ' . $response->getStatusCode()); + } + if ($response->getStatusCode() == 304) { + $responseXml = $data; + } else { + $responseXml = $response->getBody(); + $cache->setItem($cacheId, $responseXml); + if ($response->getHeaders()->get('ETag')) { + $cache->setItem($cacheId . '_etag', $response->getHeaders()->get('ETag')->getFieldValue()); + } + if ($response->getHeaders()->get('Last-Modified')) { + $cache->setItem($cacheId . '_lastmodified', $response->getHeaders()->get('Last-Modified')->getFieldValue()); + } + } + return static::importString($responseXml); + } elseif ($cache) { + $data = $cache->getItem($cacheId); + if ($data) { + return static::importString($data); + } + $response = $client->send(); + if ((int) $response->getStatusCode() !== 200) { + throw new Exception\RuntimeException('Feed failed to load, got response code ' . $response->getStatusCode()); + } + $responseXml = $response->getBody(); + $cache->setItem($cacheId, $responseXml); + return static::importString($responseXml); + } else { + $response = $client->send(); + if ((int) $response->getStatusCode() !== 200) { + throw new Exception\RuntimeException('Feed failed to load, got response code ' . $response->getStatusCode()); + } + $reader = static::importString($response->getBody()); + $reader->setOriginalSourceUri($uri); + return $reader; + } + } + + /** + * Import a feed from a remote URI + * + * Performs similarly to import(), except it uses the HTTP client passed to + * the method, and does not take into account cached data. + * + * Primary purpose is to make it possible to use the Reader with alternate + * HTTP client implementations. + * + * @param string $uri + * @param Http\ClientInterface $client + * @return self + * @throws Exception\RuntimeException if response is not an Http\ResponseInterface + */ + public static function importRemoteFeed($uri, Http\ClientInterface $client) + { + $response = $client->get($uri); + if (!$response instanceof Http\ResponseInterface) { + throw new Exception\RuntimeException(sprintf( + 'Did not receive a %s\Http\ResponseInterface from the provided HTTP client; received "%s"', + __NAMESPACE__, + (is_object($response) ? get_class($response) : gettype($response)) + )); + } + + if ((int) $response->getStatusCode() !== 200) { + throw new Exception\RuntimeException('Feed failed to load, got response code ' . $response->getStatusCode()); + } + $reader = static::importString($response->getBody()); + $reader->setOriginalSourceUri($uri); + return $reader; + } + + /** + * Import a feed from a string + * + * @param string $string + * @return Feed\FeedInterface + * @throws Exception\InvalidArgumentException + * @throws Exception\RuntimeException + */ + public static function importString($string) + { + $trimmed = trim($string); + if (!is_string($string) || empty($trimmed)) { + throw new Exception\InvalidArgumentException('Only non empty strings are allowed as input'); + } + + $libxmlErrflag = libxml_use_internal_errors(true); + $oldValue = libxml_disable_entity_loader(true); + $dom = new DOMDocument; + $status = $dom->loadXML(trim($string)); + foreach ($dom->childNodes as $child) { + if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) { + throw new Exception\InvalidArgumentException( + 'Invalid XML: Detected use of illegal DOCTYPE' + ); + } + } + libxml_disable_entity_loader($oldValue); + libxml_use_internal_errors($libxmlErrflag); + + if (!$status) { + // Build error message + $error = libxml_get_last_error(); + if ($error && $error->message) { + $error->message = trim($error->message); + $errormsg = "DOMDocument cannot parse XML: {$error->message}"; + } else { + $errormsg = "DOMDocument cannot parse XML: Please check the XML document's validity"; + } + throw new Exception\RuntimeException($errormsg); + } + + $type = static::detectType($dom); + + static::registerCoreExtensions(); + + if (substr($type, 0, 3) == 'rss') { + $reader = new Feed\Rss($dom, $type); + } elseif (substr($type, 8, 5) == 'entry') { + $reader = new Entry\Atom($dom->documentElement, 0, self::TYPE_ATOM_10); + } elseif (substr($type, 0, 4) == 'atom') { + $reader = new Feed\Atom($dom, $type); + } else { + throw new Exception\RuntimeException('The URI used does not point to a ' + . 'valid Atom, RSS or RDF feed that Zend\Feed\Reader can parse.'); + } + return $reader; + } + + /** + * Imports a feed from a file located at $filename. + * + * @param string $filename + * @throws Exception\RuntimeException + * @return Feed\FeedInterface + */ + public static function importFile($filename) + { + ErrorHandler::start(); + $feed = file_get_contents($filename); + $err = ErrorHandler::stop(); + if ($feed === false) { + throw new Exception\RuntimeException("File '{$filename}' could not be loaded", 0, $err); + } + return static::importString($feed); + } + + /** + * Find feed links + * + * @param $uri + * @return FeedSet + * @throws Exception\RuntimeException + */ + public static function findFeedLinks($uri) + { + $client = static::getHttpClient(); + $client->setUri($uri); + $response = $client->send(); + if ($response->getStatusCode() !== 200) { + throw new Exception\RuntimeException("Failed to access $uri, got response code " . $response->getStatusCode()); + } + $responseHtml = $response->getBody(); + $libxmlErrflag = libxml_use_internal_errors(true); + $oldValue = libxml_disable_entity_loader(true); + $dom = new DOMDocument; + $status = $dom->loadHTML(trim($responseHtml)); + libxml_disable_entity_loader($oldValue); + libxml_use_internal_errors($libxmlErrflag); + if (!$status) { + // Build error message + $error = libxml_get_last_error(); + if ($error && $error->message) { + $error->message = trim($error->message); + $errormsg = "DOMDocument cannot parse HTML: {$error->message}"; + } else { + $errormsg = "DOMDocument cannot parse HTML: Please check the XML document's validity"; + } + throw new Exception\RuntimeException($errormsg); + } + $feedSet = new FeedSet; + $links = $dom->getElementsByTagName('link'); + $feedSet->addLinks($links, $uri); + return $feedSet; + } + + /** + * Detect the feed type of the provided feed + * + * @param Feed\AbstractFeed|DOMDocument|string $feed + * @param bool $specOnly + * @return string + * @throws Exception\InvalidArgumentException + * @throws Exception\RuntimeException + */ + public static function detectType($feed, $specOnly = false) + { + if ($feed instanceof Feed\AbstractFeed) { + $dom = $feed->getDomDocument(); + } elseif ($feed instanceof DOMDocument) { + $dom = $feed; + } elseif (is_string($feed) && !empty($feed)) { + ErrorHandler::start(E_NOTICE|E_WARNING); + ini_set('track_errors', 1); + $oldValue = libxml_disable_entity_loader(true); + $dom = new DOMDocument; + $status = $dom->loadXML($feed); + foreach ($dom->childNodes as $child) { + if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) { + throw new Exception\InvalidArgumentException( + 'Invalid XML: Detected use of illegal DOCTYPE' + ); + } + } + libxml_disable_entity_loader($oldValue); + ini_restore('track_errors'); + ErrorHandler::stop(); + if (!$status) { + if (!isset($phpErrormsg)) { + if (function_exists('xdebug_is_enabled')) { + $phpErrormsg = '(error message not available, when XDebug is running)'; + } else { + $phpErrormsg = '(error message not available)'; + } + } + throw new Exception\RuntimeException("DOMDocument cannot parse XML: $phpErrormsg"); + } + } else { + throw new Exception\InvalidArgumentException('Invalid object/scalar provided: must' + . ' be of type Zend\Feed\Reader\Feed, DomDocument or string'); + } + $xpath = new DOMXPath($dom); + + if ($xpath->query('/rss')->length) { + $type = self::TYPE_RSS_ANY; + $version = $xpath->evaluate('string(/rss/@version)'); + + if (strlen($version) > 0) { + switch ($version) { + case '2.0': + $type = self::TYPE_RSS_20; + break; + + case '0.94': + $type = self::TYPE_RSS_094; + break; + + case '0.93': + $type = self::TYPE_RSS_093; + break; + + case '0.92': + $type = self::TYPE_RSS_092; + break; + + case '0.91': + $type = self::TYPE_RSS_091; + break; + } + } + + return $type; + } + + $xpath->registerNamespace('rdf', self::NAMESPACE_RDF); + + if ($xpath->query('/rdf:RDF')->length) { + $xpath->registerNamespace('rss', self::NAMESPACE_RSS_10); + + if ($xpath->query('/rdf:RDF/rss:channel')->length + || $xpath->query('/rdf:RDF/rss:image')->length + || $xpath->query('/rdf:RDF/rss:item')->length + || $xpath->query('/rdf:RDF/rss:textinput')->length + ) { + return self::TYPE_RSS_10; + } + + $xpath->registerNamespace('rss', self::NAMESPACE_RSS_090); + + if ($xpath->query('/rdf:RDF/rss:channel')->length + || $xpath->query('/rdf:RDF/rss:image')->length + || $xpath->query('/rdf:RDF/rss:item')->length + || $xpath->query('/rdf:RDF/rss:textinput')->length + ) { + return self::TYPE_RSS_090; + } + } + + $xpath->registerNamespace('atom', self::NAMESPACE_ATOM_10); + + if ($xpath->query('//atom:feed')->length) { + return self::TYPE_ATOM_10; + } + + if ($xpath->query('//atom:entry')->length) { + if ($specOnly == true) { + return self::TYPE_ATOM_10; + } else { + return self::TYPE_ATOM_10_ENTRY; + } + } + + $xpath->registerNamespace('atom', self::NAMESPACE_ATOM_03); + + if ($xpath->query('//atom:feed')->length) { + return self::TYPE_ATOM_03; + } + + return self::TYPE_ANY; + } + + /** + * Set plugin manager for use with Extensions + * + * @param ExtensionManagerInterface $extensionManager + */ + public static function setExtensionManager(ExtensionManagerInterface $extensionManager) + { + static::$extensionManager = $extensionManager; + } + + /** + * Get plugin manager for use with Extensions + * + * @return ExtensionManagerInterface + */ + public static function getExtensionManager() + { + if (!isset(static::$extensionManager)) { + static::setExtensionManager(new ExtensionManager()); + } + return static::$extensionManager; + } + + /** + * Register an Extension by name + * + * @param string $name + * @return void + * @throws Exception\RuntimeException if unable to resolve Extension class + */ + public static function registerExtension($name) + { + $feedName = $name . '\Feed'; + $entryName = $name . '\Entry'; + $manager = static::getExtensionManager(); + if (static::isRegistered($name)) { + if ($manager->has($feedName) || $manager->has($entryName)) { + return; + } + } + + if (!$manager->has($feedName) && !$manager->has($entryName)) { + throw new Exception\RuntimeException('Could not load extension: ' . $name + . ' using Plugin Loader. Check prefix paths are configured and extension exists.'); + } + if ($manager->has($feedName)) { + static::$extensions['feed'][] = $feedName; + } + if ($manager->has($entryName)) { + static::$extensions['entry'][] = $entryName; + } + } + + /** + * Is a given named Extension registered? + * + * @param string $extensionName + * @return bool + */ + public static function isRegistered($extensionName) + { + $feedName = $extensionName . '\Feed'; + $entryName = $extensionName . '\Entry'; + if (in_array($feedName, static::$extensions['feed']) + || in_array($entryName, static::$extensions['entry']) + ) { + return true; + } + return false; + } + + /** + * Get a list of extensions + * + * @return array + */ + public static function getExtensions() + { + return static::$extensions; + } + + /** + * Reset class state to defaults + * + * @return void + */ + public static function reset() + { + static::$cache = null; + static::$httpClient = null; + static::$httpMethodOverride = false; + static::$httpConditionalGet = false; + static::$extensionManager = null; + static::$extensions = array( + 'feed' => array( + 'DublinCore\Feed', + 'Atom\Feed' + ), + 'entry' => array( + 'Content\Entry', + 'DublinCore\Entry', + 'Atom\Entry' + ), + 'core' => array( + 'DublinCore\Feed', + 'Atom\Feed', + 'Content\Entry', + 'DublinCore\Entry', + 'Atom\Entry' + ) + ); + } + + /** + * Register core (default) extensions + * + * @return void + */ + protected static function registerCoreExtensions() + { + static::registerExtension('DublinCore'); + static::registerExtension('Content'); + static::registerExtension('Atom'); + static::registerExtension('Slash'); + static::registerExtension('WellFormedWeb'); + static::registerExtension('Thread'); + static::registerExtension('Podcast'); + } + + /** + * Utility method to apply array_unique operation to a multidimensional + * array. + * + * @param array + * @return array + */ + public static function arrayUnique(array $array) + { + foreach ($array as &$value) { + $value = serialize($value); + } + $array = array_unique($array); + foreach ($array as &$value) { + $value = unserialize($value); + } + return $array; + } +} diff --git a/library/Zend/Feed/Uri.php b/library/Zend/Feed/Uri.php new file mode 100755 index 0000000000..940bce11ab --- /dev/null +++ b/library/Zend/Feed/Uri.php @@ -0,0 +1,184 @@ +valid = false; + return; + } + + $this->scheme = isset($parsed['scheme']) ? $parsed['scheme'] : null; + $this->host = isset($parsed['host']) ? $parsed['host'] : null; + $this->port = isset($parsed['port']) ? $parsed['port'] : null; + $this->user = isset($parsed['user']) ? $parsed['user'] : null; + $this->pass = isset($parsed['pass']) ? $parsed['pass'] : null; + $this->path = isset($parsed['path']) ? $parsed['path'] : null; + $this->query = isset($parsed['query']) ? $parsed['query'] : null; + $this->fragment = isset($parsed['fragment']) ? $parsed['fragment'] : null; + } + + /** + * Create an instance + * + * Useful for chained validations + * + * @param string $uri + * @return self + */ + public static function factory($uri) + { + return new static($uri); + } + + /** + * Retrieve the host + * + * @return string + */ + public function getHost() + { + return $this->host; + } + + /** + * Retrieve the URI path + * + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * Retrieve the scheme + * + * @return string + */ + public function getScheme() + { + return $this->scheme; + } + + /** + * Is the URI valid? + * + * @return bool + */ + public function isValid() + { + if (false === $this->valid) { + return false; + } + + if ($this->scheme && !in_array($this->scheme, $this->validSchemes)) { + return false; + } + + if ($this->host) { + if ($this->path && substr($this->path, 0, 1) != '/') { + return false; + } + return true; + } + + // no host, but user and/or port... what? + if ($this->user || $this->port) { + return false; + } + + if ($this->path) { + // Check path-only (no host) URI + if (substr($this->path, 0, 2) == '//') { + return false; + } + return true; + } + + if (! ($this->query || $this->fragment)) { + // No host, path, query or fragment - this is not a valid URI + return false; + } + + return true; + } + + /** + * Is the URI absolute? + * + * @return bool + */ + public function isAbsolute() + { + return ($this->scheme !== null); + } +} diff --git a/library/Zend/Feed/Writer/AbstractFeed.php b/library/Zend/Feed/Writer/AbstractFeed.php new file mode 100755 index 0000000000..1dd6fe54b6 --- /dev/null +++ b/library/Zend/Feed/Writer/AbstractFeed.php @@ -0,0 +1,846 @@ +_loadExtensions(); + } + + /** + * Set a single author + * + * The following option keys are supported: + * 'name' => (string) The name + * 'email' => (string) An optional email + * 'uri' => (string) An optional and valid URI + * + * @param array $author + * @throws Exception\InvalidArgumentException If any value of $author not follow the format. + * @return AbstractFeed + */ + public function addAuthor(array $author) + { + // Check array values + if (!array_key_exists('name', $author) + || empty($author['name']) + || !is_string($author['name']) + ) { + throw new Exception\InvalidArgumentException( + 'Invalid parameter: author array must include a "name" key with a non-empty string value'); + } + + if (isset($author['email'])) { + if (empty($author['email']) || !is_string($author['email'])) { + throw new Exception\InvalidArgumentException( + 'Invalid parameter: "email" array value must be a non-empty string'); + } + } + if (isset($author['uri'])) { + if (empty($author['uri']) || !is_string($author['uri']) || + !Uri::factory($author['uri'])->isValid() + ) { + throw new Exception\InvalidArgumentException( + 'Invalid parameter: "uri" array value must be a non-empty string and valid URI/IRI'); + } + } + + $this->data['authors'][] = $author; + + return $this; + } + + /** + * Set an array with feed authors + * + * @see addAuthor + * @param array $authors + * @return AbstractFeed + */ + public function addAuthors(array $authors) + { + foreach ($authors as $author) { + $this->addAuthor($author); + } + + return $this; + } + + /** + * Set the copyright entry + * + * @param string $copyright + * @throws Exception\InvalidArgumentException + * @return AbstractFeed + */ + public function setCopyright($copyright) + { + if (empty($copyright) || !is_string($copyright)) { + throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string'); + } + $this->data['copyright'] = $copyright; + + return $this; + } + + /** + * Set the feed creation date + * + * @param null|int|DateTime + * @throws Exception\InvalidArgumentException + * @return AbstractFeed + */ + public function setDateCreated($date = null) + { + if ($date === null) { + $date = new DateTime(); + } elseif (is_int($date)) { + $date = new DateTime('@' . $date); + } elseif (!$date instanceof DateTime) { + throw new Exception\InvalidArgumentException('Invalid DateTime object or UNIX Timestamp' + . ' passed as parameter'); + } + $this->data['dateCreated'] = $date; + + return $this; + } + + /** + * Set the feed modification date + * + * @param null|int|DateTime + * @throws Exception\InvalidArgumentException + * @return AbstractFeed + */ + public function setDateModified($date = null) + { + if ($date === null) { + $date = new DateTime(); + } elseif (is_int($date)) { + $date = new DateTime('@' . $date); + } elseif (!$date instanceof DateTime) { + throw new Exception\InvalidArgumentException('Invalid DateTime object or UNIX Timestamp' + . ' passed as parameter'); + } + $this->data['dateModified'] = $date; + + return $this; + } + + /** + * Set the feed last-build date. Ignored for Atom 1.0. + * + * @param null|int|DateTime + * @throws Exception\InvalidArgumentException + * @return AbstractFeed + */ + public function setLastBuildDate($date = null) + { + if ($date === null) { + $date = new DateTime(); + } elseif (is_int($date)) { + $date = new DateTime('@' . $date); + } elseif (!$date instanceof DateTime) { + throw new Exception\InvalidArgumentException('Invalid DateTime object or UNIX Timestamp' + . ' passed as parameter'); + } + $this->data['lastBuildDate'] = $date; + + return $this; + } + + /** + * Set the feed description + * + * @param string $description + * @throws Exception\InvalidArgumentException + * @return AbstractFeed + */ + public function setDescription($description) + { + if (empty($description) || !is_string($description)) { + throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string'); + } + $this->data['description'] = $description; + + return $this; + } + + /** + * Set the feed generator entry + * + * @param array|string $name + * @param null|string $version + * @param null|string $uri + * @throws Exception\InvalidArgumentException + * @return AbstractFeed + */ + public function setGenerator($name, $version = null, $uri = null) + { + if (is_array($name)) { + $data = $name; + if (empty($data['name']) || !is_string($data['name'])) { + throw new Exception\InvalidArgumentException('Invalid parameter: "name" must be a non-empty string'); + } + $generator = array('name' => $data['name']); + if (isset($data['version'])) { + if (empty($data['version']) || !is_string($data['version'])) { + throw new Exception\InvalidArgumentException('Invalid parameter: "version" must be a non-empty string'); + } + $generator['version'] = $data['version']; + } + if (isset($data['uri'])) { + if (empty($data['uri']) || !is_string($data['uri']) || !Uri::factory($data['uri'])->isValid()) { + throw new Exception\InvalidArgumentException('Invalid parameter: "uri" must be a non-empty string and a valid URI/IRI'); + } + $generator['uri'] = $data['uri']; + } + } else { + if (empty($name) || !is_string($name)) { + throw new Exception\InvalidArgumentException('Invalid parameter: "name" must be a non-empty string'); + } + $generator = array('name' => $name); + if (isset($version)) { + if (empty($version) || !is_string($version)) { + throw new Exception\InvalidArgumentException('Invalid parameter: "version" must be a non-empty string'); + } + $generator['version'] = $version; + } + if (isset($uri)) { + if (empty($uri) || !is_string($uri) || !Uri::factory($uri)->isValid()) { + throw new Exception\InvalidArgumentException('Invalid parameter: "uri" must be a non-empty string and a valid URI/IRI'); + } + $generator['uri'] = $uri; + } + } + $this->data['generator'] = $generator; + + return $this; + } + + /** + * Set the feed ID - URI or URN (via PCRE pattern) supported + * + * @param string $id + * @throws Exception\InvalidArgumentException + * @return AbstractFeed + */ + public function setId($id) + { + if ((empty($id) || !is_string($id) || !Uri::factory($id)->isValid()) + && !preg_match("#^urn:[a-zA-Z0-9][a-zA-Z0-9\-]{1,31}:([a-zA-Z0-9\(\)\+\,\.\:\=\@\;\$\_\!\*\-]|%[0-9a-fA-F]{2})*#", $id) + && !$this->_validateTagUri($id) + ) { + throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string and valid URI/IRI'); + } + $this->data['id'] = $id; + + return $this; + } + + /** + * Validate a URI using the tag scheme (RFC 4151) + * + * @param string $id + * @return bool + */ + protected function _validateTagUri($id) + { + if (preg_match('/^tag:(?P.*),(?P\d{4}-?\d{0,2}-?\d{0,2}):(?P.*)(.*:)*$/', $id, $matches)) { + $dvalid = false; + $date = $matches['date']; + $d6 = strtotime($date); + if ((strlen($date) == 4) && $date <= date('Y')) { + $dvalid = true; + } elseif ((strlen($date) == 7) && ($d6 < strtotime("now"))) { + $dvalid = true; + } elseif ((strlen($date) == 10) && ($d6 < strtotime("now"))) { + $dvalid = true; + } + $validator = new Validator\EmailAddress; + if ($validator->isValid($matches['name'])) { + $nvalid = true; + } else { + $nvalid = $validator->isValid('info@' . $matches['name']); + } + return $dvalid && $nvalid; + } + return false; + } + + /** + * Set a feed image (URI at minimum). Parameter is a single array with the + * required key 'uri'. When rendering as RSS, the required keys are 'uri', + * 'title' and 'link'. RSS also specifies three optional parameters 'width', + * 'height' and 'description'. Only 'uri' is required and used for Atom rendering. + * + * @param array $data + * @throws Exception\InvalidArgumentException + * @return AbstractFeed + */ + public function setImage(array $data) + { + if (empty($data['uri']) || !is_string($data['uri']) + || !Uri::factory($data['uri'])->isValid() + ) { + throw new Exception\InvalidArgumentException('Invalid parameter: parameter \'uri\'' + . ' must be a non-empty string and valid URI/IRI'); + } + $this->data['image'] = $data; + + return $this; + } + + /** + * Set the feed language + * + * @param string $language + * @throws Exception\InvalidArgumentException + * @return AbstractFeed + */ + public function setLanguage($language) + { + if (empty($language) || !is_string($language)) { + throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string'); + } + $this->data['language'] = $language; + + return $this; + } + + /** + * Set a link to the HTML source + * + * @param string $link + * @throws Exception\InvalidArgumentException + * @return AbstractFeed + */ + public function setLink($link) + { + if (empty($link) || !is_string($link) || !Uri::factory($link)->isValid()) { + throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string and valid URI/IRI'); + } + $this->data['link'] = $link; + + return $this; + } + + /** + * Set a link to an XML feed for any feed type/version + * + * @param string $link + * @param string $type + * @throws Exception\InvalidArgumentException + * @return AbstractFeed + */ + public function setFeedLink($link, $type) + { + if (empty($link) || !is_string($link) || !Uri::factory($link)->isValid()) { + throw new Exception\InvalidArgumentException('Invalid parameter: "link"" must be a non-empty string and valid URI/IRI'); + } + if (!in_array(strtolower($type), array('rss', 'rdf', 'atom'))) { + throw new Exception\InvalidArgumentException('Invalid parameter: "type"; You must declare the type of feed the link points to, i.e. RSS, RDF or Atom'); + } + $this->data['feedLinks'][strtolower($type)] = $link; + + return $this; + } + + /** + * Set the feed title + * + * @param string $title + * @throws Exception\InvalidArgumentException + * @return AbstractFeed + */ + public function setTitle($title) + { + if (empty($title) || !is_string($title)) { + throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string'); + } + $this->data['title'] = $title; + + return $this; + } + + /** + * Set the feed character encoding + * + * @param string $encoding + * @throws Exception\InvalidArgumentException + * @return AbstractFeed + */ + public function setEncoding($encoding) + { + if (empty($encoding) || !is_string($encoding)) { + throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string'); + } + $this->data['encoding'] = $encoding; + + return $this; + } + + /** + * Set the feed's base URL + * + * @param string $url + * @throws Exception\InvalidArgumentException + * @return AbstractFeed + */ + public function setBaseUrl($url) + { + if (empty($url) || !is_string($url) || !Uri::factory($url)->isValid()) { + throw new Exception\InvalidArgumentException('Invalid parameter: "url" array value' + . ' must be a non-empty string and valid URI/IRI'); + } + $this->data['baseUrl'] = $url; + + return $this; + } + + /** + * Add a Pubsubhubbub hub endpoint URL + * + * @param string $url + * @throws Exception\InvalidArgumentException + * @return AbstractFeed + */ + public function addHub($url) + { + if (empty($url) || !is_string($url) || !Uri::factory($url)->isValid()) { + throw new Exception\InvalidArgumentException('Invalid parameter: "url" array value' + . ' must be a non-empty string and valid URI/IRI'); + } + if (!isset($this->data['hubs'])) { + $this->data['hubs'] = array(); + } + $this->data['hubs'][] = $url; + + return $this; + } + + /** + * Add Pubsubhubbub hub endpoint URLs + * + * @param array $urls + * @return AbstractFeed + */ + public function addHubs(array $urls) + { + foreach ($urls as $url) { + $this->addHub($url); + } + + return $this; + } + + /** + * Add a feed category + * + * @param array $category + * @throws Exception\InvalidArgumentException + * @return AbstractFeed + */ + public function addCategory(array $category) + { + if (!isset($category['term'])) { + throw new Exception\InvalidArgumentException('Each category must be an array and ' + . 'contain at least a "term" element containing the machine ' + . ' readable category name'); + } + if (isset($category['scheme'])) { + if (empty($category['scheme']) + || !is_string($category['scheme']) + || !Uri::factory($category['scheme'])->isValid() + ) { + throw new Exception\InvalidArgumentException('The Atom scheme or RSS domain of' + . ' a category must be a valid URI'); + } + } + if (!isset($this->data['categories'])) { + $this->data['categories'] = array(); + } + $this->data['categories'][] = $category; + + return $this; + } + + /** + * Set an array of feed categories + * + * @param array $categories + * @return AbstractFeed + */ + public function addCategories(array $categories) + { + foreach ($categories as $category) { + $this->addCategory($category); + } + + return $this; + } + + /** + * Get a single author + * + * @param int $index + * @return string|null + */ + public function getAuthor($index = 0) + { + if (isset($this->data['authors'][$index])) { + return $this->data['authors'][$index]; + } + + return null; + } + + /** + * Get an array with feed authors + * + * @return array + */ + public function getAuthors() + { + if (!array_key_exists('authors', $this->data)) { + return null; + } + return $this->data['authors']; + } + + /** + * Get the copyright entry + * + * @return string|null + */ + public function getCopyright() + { + if (!array_key_exists('copyright', $this->data)) { + return null; + } + return $this->data['copyright']; + } + + /** + * Get the feed creation date + * + * @return string|null + */ + public function getDateCreated() + { + if (!array_key_exists('dateCreated', $this->data)) { + return null; + } + return $this->data['dateCreated']; + } + + /** + * Get the feed modification date + * + * @return string|null + */ + public function getDateModified() + { + if (!array_key_exists('dateModified', $this->data)) { + return null; + } + return $this->data['dateModified']; + } + + /** + * Get the feed last-build date + * + * @return string|null + */ + public function getLastBuildDate() + { + if (!array_key_exists('lastBuildDate', $this->data)) { + return null; + } + return $this->data['lastBuildDate']; + } + + /** + * Get the feed description + * + * @return string|null + */ + public function getDescription() + { + if (!array_key_exists('description', $this->data)) { + return null; + } + return $this->data['description']; + } + + /** + * Get the feed generator entry + * + * @return string|null + */ + public function getGenerator() + { + if (!array_key_exists('generator', $this->data)) { + return null; + } + return $this->data['generator']; + } + + /** + * Get the feed ID + * + * @return string|null + */ + public function getId() + { + if (!array_key_exists('id', $this->data)) { + return null; + } + return $this->data['id']; + } + + /** + * Get the feed image URI + * + * @return array + */ + public function getImage() + { + if (!array_key_exists('image', $this->data)) { + return null; + } + return $this->data['image']; + } + + /** + * Get the feed language + * + * @return string|null + */ + public function getLanguage() + { + if (!array_key_exists('language', $this->data)) { + return null; + } + return $this->data['language']; + } + + /** + * Get a link to the HTML source + * + * @return string|null + */ + public function getLink() + { + if (!array_key_exists('link', $this->data)) { + return null; + } + return $this->data['link']; + } + + /** + * Get a link to the XML feed + * + * @return string|null + */ + public function getFeedLinks() + { + if (!array_key_exists('feedLinks', $this->data)) { + return null; + } + return $this->data['feedLinks']; + } + + /** + * Get the feed title + * + * @return string|null + */ + public function getTitle() + { + if (!array_key_exists('title', $this->data)) { + return null; + } + return $this->data['title']; + } + + /** + * Get the feed character encoding + * + * @return string|null + */ + public function getEncoding() + { + if (!array_key_exists('encoding', $this->data)) { + return 'UTF-8'; + } + return $this->data['encoding']; + } + + /** + * Get the feed's base url + * + * @return string|null + */ + public function getBaseUrl() + { + if (!array_key_exists('baseUrl', $this->data)) { + return null; + } + return $this->data['baseUrl']; + } + + /** + * Get the URLs used as Pubsubhubbub hubs endpoints + * + * @return string|null + */ + public function getHubs() + { + if (!array_key_exists('hubs', $this->data)) { + return null; + } + return $this->data['hubs']; + } + + /** + * Get the feed categories + * + * @return string|null + */ + public function getCategories() + { + if (!array_key_exists('categories', $this->data)) { + return null; + } + return $this->data['categories']; + } + + /** + * Resets the instance and deletes all data + * + * @return void + */ + public function reset() + { + $this->data = array(); + } + + /** + * Set the current feed type being exported to "rss" or "atom". This allows + * other objects to gracefully choose whether to execute or not, depending + * on their appropriateness for the current type, e.g. renderers. + * + * @param string $type + * @return AbstractFeed + */ + public function setType($type) + { + $this->type = $type; + return $this; + } + + /** + * Retrieve the current or last feed type exported. + * + * @return string Value will be "rss" or "atom" + */ + public function getType() + { + return $this->type; + } + + /** + * Unset a specific data point + * + * @param string $name + * @return AbstractFeed + */ + public function remove($name) + { + if (isset($this->data[$name])) { + unset($this->data[$name]); + } + return $this; + } + + /** + * Method overloading: call given method on first extension implementing it + * + * @param string $method + * @param array $args + * @return mixed + * @throws Exception\BadMethodCallException if no extensions implements the method + */ + public function __call($method, $args) + { + foreach ($this->extensions as $extension) { + try { + return call_user_func_array(array($extension, $method), $args); + } catch (Exception\BadMethodCallException $e) { + } + } + throw new Exception\BadMethodCallException( + 'Method: ' . $method . ' does not exist and could not be located on a registered Extension' + ); + } + + /** + * Load extensions from Zend\Feed\Writer\Writer + * + * @throws Exception\RuntimeException + * @return void + */ + protected function _loadExtensions() + { + $all = Writer::getExtensions(); + $manager = Writer::getExtensionManager(); + $exts = $all['feed']; + foreach ($exts as $ext) { + if (!$manager->has($ext)) { + throw new Exception\RuntimeException(sprintf('Unable to load extension "%s"; could not resolve to class', $ext)); + } + $this->extensions[$ext] = $manager->get($ext); + $this->extensions[$ext]->setEncoding($this->getEncoding()); + } + } +} diff --git a/library/Zend/Feed/Writer/Deleted.php b/library/Zend/Feed/Writer/Deleted.php new file mode 100755 index 0000000000..b91ee09452 --- /dev/null +++ b/library/Zend/Feed/Writer/Deleted.php @@ -0,0 +1,236 @@ +data['encoding'] = $encoding; + + return $this; + } + + /** + * Get the feed character encoding + * + * @return string|null + */ + public function getEncoding() + { + if (!array_key_exists('encoding', $this->data)) { + return 'UTF-8'; + } + return $this->data['encoding']; + } + + /** + * Unset a specific data point + * + * @param string $name + * @return Deleted + */ + public function remove($name) + { + if (isset($this->data[$name])) { + unset($this->data[$name]); + } + + return $this; + } + + /** + * Set the current feed type being exported to "rss" or "atom". This allows + * other objects to gracefully choose whether to execute or not, depending + * on their appropriateness for the current type, e.g. renderers. + * + * @param string $type + * @return Deleted + */ + public function setType($type) + { + $this->type = $type; + return $this; + } + + /** + * Retrieve the current or last feed type exported. + * + * @return string Value will be "rss" or "atom" + */ + public function getType() + { + return $this->type; + } + + /** + * Set reference + * + * @param $reference + * @throws Exception\InvalidArgumentException + * @return Deleted + */ + public function setReference($reference) + { + if (empty($reference) || !is_string($reference)) { + throw new Exception\InvalidArgumentException('Invalid parameter: reference must be a non-empty string'); + } + $this->data['reference'] = $reference; + + return $this; + } + + /** + * @return string + */ + public function getReference() + { + if (!array_key_exists('reference', $this->data)) { + return null; + } + return $this->data['reference']; + } + + /** + * Set when + * + * @param null|string|DateTime $date + * @throws Exception\InvalidArgumentException + * @return Deleted + */ + public function setWhen($date = null) + { + if ($date === null) { + $date = new DateTime(); + } elseif (is_int($date)) { + $date = new DateTime('@' . $date); + } elseif (!$date instanceof DateTime) { + throw new Exception\InvalidArgumentException('Invalid DateTime object or UNIX Timestamp' + . ' passed as parameter'); + } + $this->data['when'] = $date; + + return $this; + } + + /** + * @return DateTime + */ + public function getWhen() + { + if (!array_key_exists('when', $this->data)) { + return null; + } + return $this->data['when']; + } + + /** + * Set by + * + * @param array $by + * @throws Exception\InvalidArgumentException + * @return Deleted + */ + public function setBy(array $by) + { + $author = array(); + if (!array_key_exists('name', $by) + || empty($by['name']) + || !is_string($by['name']) + ) { + throw new Exception\InvalidArgumentException('Invalid parameter: author array must include a' + . ' "name" key with a non-empty string value'); + } + $author['name'] = $by['name']; + if (isset($by['email'])) { + if (empty($by['email']) || !is_string($by['email'])) { + throw new Exception\InvalidArgumentException('Invalid parameter: "email" array' + . ' value must be a non-empty string'); + } + $author['email'] = $by['email']; + } + if (isset($by['uri'])) { + if (empty($by['uri']) + || !is_string($by['uri']) + || !Uri::factory($by['uri'])->isValid() + ) { + throw new Exception\InvalidArgumentException('Invalid parameter: "uri" array value must' + . ' be a non-empty string and valid URI/IRI'); + } + $author['uri'] = $by['uri']; + } + $this->data['by'] = $author; + + return $this; + } + + /** + * @return string + */ + public function getBy() + { + if (!array_key_exists('by', $this->data)) { + return null; + } + return $this->data['by']; + } + + /** + * @param string $comment + * @return Deleted + */ + public function setComment($comment) + { + $this->data['comment'] = $comment; + return $this; + } + + /** + * @return string + */ + public function getComment() + { + if (!array_key_exists('comment', $this->data)) { + return null; + } + return $this->data['comment']; + } +} diff --git a/library/Zend/Feed/Writer/Entry.php b/library/Zend/Feed/Writer/Entry.php new file mode 100755 index 0000000000..c5b1085ace --- /dev/null +++ b/library/Zend/Feed/Writer/Entry.php @@ -0,0 +1,765 @@ +_loadExtensions(); + } + + /** + * Set a single author + * + * The following option keys are supported: + * 'name' => (string) The name + * 'email' => (string) An optional email + * 'uri' => (string) An optional and valid URI + * + * @param array $author + * @throws Exception\InvalidArgumentException If any value of $author not follow the format. + * @return Entry + */ + public function addAuthor(array $author) + { + // Check array values + if (!array_key_exists('name', $author) + || empty($author['name']) + || !is_string($author['name']) + ) { + throw new Exception\InvalidArgumentException( + 'Invalid parameter: author array must include a "name" key with a non-empty string value'); + } + + if (isset($author['email'])) { + if (empty($author['email']) || !is_string($author['email'])) { + throw new Exception\InvalidArgumentException( + 'Invalid parameter: "email" array value must be a non-empty string'); + } + } + if (isset($author['uri'])) { + if (empty($author['uri']) || !is_string($author['uri']) || + !Uri::factory($author['uri'])->isValid() + ) { + throw new Exception\InvalidArgumentException( + 'Invalid parameter: "uri" array value must be a non-empty string and valid URI/IRI'); + } + } + + $this->data['authors'][] = $author; + + return $this; + } + + /** + * Set an array with feed authors + * + * @see addAuthor + * @param array $authors + * @return Entry + */ + public function addAuthors(array $authors) + { + foreach ($authors as $author) { + $this->addAuthor($author); + } + + return $this; + } + + /** + * Set the feed character encoding + * + * @param string $encoding + * @throws Exception\InvalidArgumentException + * @return Entry + */ + public function setEncoding($encoding) + { + if (empty($encoding) || !is_string($encoding)) { + throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string'); + } + $this->data['encoding'] = $encoding; + + return $this; + } + + /** + * Get the feed character encoding + * + * @return string|null + */ + public function getEncoding() + { + if (!array_key_exists('encoding', $this->data)) { + return 'UTF-8'; + } + return $this->data['encoding']; + } + + /** + * Set the copyright entry + * + * @param string $copyright + * @throws Exception\InvalidArgumentException + * @return Entry + */ + public function setCopyright($copyright) + { + if (empty($copyright) || !is_string($copyright)) { + throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string'); + } + $this->data['copyright'] = $copyright; + + return $this; + } + + /** + * Set the entry's content + * + * @param string $content + * @throws Exception\InvalidArgumentException + * @return Entry + */ + public function setContent($content) + { + if (empty($content) || !is_string($content)) { + throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string'); + } + $this->data['content'] = $content; + + return $this; + } + + /** + * Set the feed creation date + * + * @param null|int|DateTime $date + * @throws Exception\InvalidArgumentException + * @return Entry + */ + public function setDateCreated($date = null) + { + if ($date === null) { + $date = new DateTime(); + } elseif (is_int($date)) { + $date = new DateTime('@' . $date); + } elseif (!$date instanceof DateTime) { + throw new Exception\InvalidArgumentException('Invalid DateTime object or UNIX Timestamp passed as parameter'); + } + $this->data['dateCreated'] = $date; + + return $this; + } + + /** + * Set the feed modification date + * + * @param null|int|DateTime $date + * @throws Exception\InvalidArgumentException + * @return Entry + */ + public function setDateModified($date = null) + { + if ($date === null) { + $date = new DateTime(); + } elseif (is_int($date)) { + $date = new DateTime('@' . $date); + } elseif (!$date instanceof DateTime) { + throw new Exception\InvalidArgumentException('Invalid DateTime object or UNIX Timestamp passed as parameter'); + } + $this->data['dateModified'] = $date; + + return $this; + } + + /** + * Set the feed description + * + * @param string $description + * @throws Exception\InvalidArgumentException + * @return Entry + */ + public function setDescription($description) + { + if (empty($description) || !is_string($description)) { + throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string'); + } + $this->data['description'] = $description; + + return $this; + } + + /** + * Set the feed ID + * + * @param string $id + * @throws Exception\InvalidArgumentException + * @return Entry + */ + public function setId($id) + { + if (empty($id) || !is_string($id)) { + throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string'); + } + $this->data['id'] = $id; + + return $this; + } + + /** + * Set a link to the HTML source of this entry + * + * @param string $link + * @throws Exception\InvalidArgumentException + * @return Entry + */ + public function setLink($link) + { + if (empty($link) || !is_string($link) || !Uri::factory($link)->isValid()) { + throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string and valid URI/IRI'); + } + $this->data['link'] = $link; + + return $this; + } + + /** + * Set the number of comments associated with this entry + * + * @param int $count + * @throws Exception\InvalidArgumentException + * @return Entry + */ + public function setCommentCount($count) + { + if (!is_numeric($count) || (int) $count != $count || (int) $count < 0) { + throw new Exception\InvalidArgumentException('Invalid parameter: "count" must be a positive integer number or zero'); + } + $this->data['commentCount'] = (int) $count; + + return $this; + } + + /** + * Set a link to a HTML page containing comments associated with this entry + * + * @param string $link + * @throws Exception\InvalidArgumentException + * @return Entry + */ + public function setCommentLink($link) + { + if (empty($link) || !is_string($link) || !Uri::factory($link)->isValid()) { + throw new Exception\InvalidArgumentException('Invalid parameter: "link" must be a non-empty string and valid URI/IRI'); + } + $this->data['commentLink'] = $link; + + return $this; + } + + /** + * Set a link to an XML feed for any comments associated with this entry + * + * @param array $link + * @throws Exception\InvalidArgumentException + * @return Entry + */ + public function setCommentFeedLink(array $link) + { + if (!isset($link['uri']) || !is_string($link['uri']) || !Uri::factory($link['uri'])->isValid()) { + throw new Exception\InvalidArgumentException('Invalid parameter: "link" must be a non-empty string and valid URI/IRI'); + } + if (!isset($link['type']) || !in_array($link['type'], array('atom', 'rss', 'rdf'))) { + throw new Exception\InvalidArgumentException('Invalid parameter: "type" must be one' + . ' of "atom", "rss" or "rdf"'); + } + if (!isset($this->data['commentFeedLinks'])) { + $this->data['commentFeedLinks'] = array(); + } + $this->data['commentFeedLinks'][] = $link; + + return $this; + } + + /** + * Set a links to an XML feed for any comments associated with this entry. + * Each link is an array with keys "uri" and "type", where type is one of: + * "atom", "rss" or "rdf". + * + * @param array $links + * @return Entry + */ + public function setCommentFeedLinks(array $links) + { + foreach ($links as $link) { + $this->setCommentFeedLink($link); + } + + return $this; + } + + /** + * Set the feed title + * + * @param string $title + * @throws Exception\InvalidArgumentException + * @return Entry + */ + public function setTitle($title) + { + if (empty($title) || !is_string($title)) { + throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string'); + } + $this->data['title'] = $title; + + return $this; + } + + /** + * Get an array with feed authors + * + * @return array + */ + public function getAuthors() + { + if (!array_key_exists('authors', $this->data)) { + return null; + } + return $this->data['authors']; + } + + /** + * Get the entry content + * + * @return string + */ + public function getContent() + { + if (!array_key_exists('content', $this->data)) { + return null; + } + return $this->data['content']; + } + + /** + * Get the entry copyright information + * + * @return string + */ + public function getCopyright() + { + if (!array_key_exists('copyright', $this->data)) { + return null; + } + return $this->data['copyright']; + } + + /** + * Get the entry creation date + * + * @return string + */ + public function getDateCreated() + { + if (!array_key_exists('dateCreated', $this->data)) { + return null; + } + return $this->data['dateCreated']; + } + + /** + * Get the entry modification date + * + * @return string + */ + public function getDateModified() + { + if (!array_key_exists('dateModified', $this->data)) { + return null; + } + return $this->data['dateModified']; + } + + /** + * Get the entry description + * + * @return string + */ + public function getDescription() + { + if (!array_key_exists('description', $this->data)) { + return null; + } + return $this->data['description']; + } + + /** + * Get the entry ID + * + * @return string + */ + public function getId() + { + if (!array_key_exists('id', $this->data)) { + return null; + } + return $this->data['id']; + } + + /** + * Get a link to the HTML source + * + * @return string|null + */ + public function getLink() + { + if (!array_key_exists('link', $this->data)) { + return null; + } + return $this->data['link']; + } + + + /** + * Get all links + * + * @return array + */ + public function getLinks() + { + if (!array_key_exists('links', $this->data)) { + return null; + } + return $this->data['links']; + } + + /** + * Get the entry title + * + * @return string + */ + public function getTitle() + { + if (!array_key_exists('title', $this->data)) { + return null; + } + return $this->data['title']; + } + + /** + * Get the number of comments/replies for current entry + * + * @return int + */ + public function getCommentCount() + { + if (!array_key_exists('commentCount', $this->data)) { + return null; + } + return $this->data['commentCount']; + } + + /** + * Returns a URI pointing to the HTML page where comments can be made on this entry + * + * @return string + */ + public function getCommentLink() + { + if (!array_key_exists('commentLink', $this->data)) { + return null; + } + return $this->data['commentLink']; + } + + /** + * Returns an array of URIs pointing to a feed of all comments for this entry + * where the array keys indicate the feed type (atom, rss or rdf). + * + * @return string + */ + public function getCommentFeedLinks() + { + if (!array_key_exists('commentFeedLinks', $this->data)) { + return null; + } + return $this->data['commentFeedLinks']; + } + + /** + * Add an entry category + * + * @param array $category + * @throws Exception\InvalidArgumentException + * @return Entry + */ + public function addCategory(array $category) + { + if (!isset($category['term'])) { + throw new Exception\InvalidArgumentException('Each category must be an array and ' + . 'contain at least a "term" element containing the machine ' + . ' readable category name'); + } + if (isset($category['scheme'])) { + if (empty($category['scheme']) + || !is_string($category['scheme']) + || !Uri::factory($category['scheme'])->isValid() + ) { + throw new Exception\InvalidArgumentException('The Atom scheme or RSS domain of' + . ' a category must be a valid URI'); + } + } + if (!isset($this->data['categories'])) { + $this->data['categories'] = array(); + } + $this->data['categories'][] = $category; + + return $this; + } + + /** + * Set an array of entry categories + * + * @param array $categories + * @return Entry + */ + public function addCategories(array $categories) + { + foreach ($categories as $category) { + $this->addCategory($category); + } + + return $this; + } + + /** + * Get the entry categories + * + * @return string|null + */ + public function getCategories() + { + if (!array_key_exists('categories', $this->data)) { + return null; + } + return $this->data['categories']; + } + + /** + * Adds an enclosure to the entry. The array parameter may contain the + * keys 'uri', 'type' and 'length'. Only 'uri' is required for Atom, though the + * others must also be provided or RSS rendering (where they are required) + * will throw an Exception. + * + * @param array $enclosure + * @throws Exception\InvalidArgumentException + * @return Entry + */ + public function setEnclosure(array $enclosure) + { + if (!isset($enclosure['uri'])) { + throw new Exception\InvalidArgumentException('Enclosure "uri" is not set'); + } + if (!Uri::factory($enclosure['uri'])->isValid()) { + throw new Exception\InvalidArgumentException('Enclosure "uri" is not a valid URI/IRI'); + } + $this->data['enclosure'] = $enclosure; + + return $this; + } + + /** + * Retrieve an array of all enclosures to be added to entry. + * + * @return array + */ + public function getEnclosure() + { + if (!array_key_exists('enclosure', $this->data)) { + return null; + } + return $this->data['enclosure']; + } + + /** + * Unset a specific data point + * + * @param string $name + * @return Entry + */ + public function remove($name) + { + if (isset($this->data[$name])) { + unset($this->data[$name]); + } + + return $this; + } + + /** + * Get registered extensions + * + * @return array + */ + public function getExtensions() + { + return $this->extensions; + } + + /** + * Return an Extension object with the matching name (postfixed with _Entry) + * + * @param string $name + * @return object + */ + public function getExtension($name) + { + if (array_key_exists($name . '\\Entry', $this->extensions)) { + return $this->extensions[$name . '\\Entry']; + } + return null; + } + + /** + * Set the current feed type being exported to "rss" or "atom". This allows + * other objects to gracefully choose whether to execute or not, depending + * on their appropriateness for the current type, e.g. renderers. + * + * @param string $type + * @return Entry + */ + public function setType($type) + { + $this->type = $type; + return $this; + } + + /** + * Retrieve the current or last feed type exported. + * + * @return string Value will be "rss" or "atom" + */ + public function getType() + { + return $this->type; + } + + /** + * Method overloading: call given method on first extension implementing it + * + * @param string $method + * @param array $args + * @return mixed + * @throws Exception\BadMethodCallException if no extensions implements the method + */ + public function __call($method, $args) + { + foreach ($this->extensions as $extension) { + try { + return call_user_func_array(array($extension, $method), $args); + } catch (\BadMethodCallException $e) { + } + } + throw new Exception\BadMethodCallException('Method: ' . $method + . ' does not exist and could not be located on a registered Extension'); + } + + /** + * Creates a new Zend\Feed\Writer\Source data container for use. This is NOT + * added to the current feed automatically, but is necessary to create a + * container with some initial values preset based on the current feed data. + * + * @return Source + */ + public function createSource() + { + $source = new Source; + if ($this->getEncoding()) { + $source->setEncoding($this->getEncoding()); + } + $source->setType($this->getType()); + return $source; + } + + /** + * Appends a Zend\Feed\Writer\Entry object representing a new entry/item + * the feed data container's internal group of entries. + * + * @param Source $source + * @return Entry + */ + public function setSource(Source $source) + { + $this->data['source'] = $source; + return $this; + } + + /** + * @return Source + */ + public function getSource() + { + if (isset($this->data['source'])) { + return $this->data['source']; + } + return null; + } + + /** + * Load extensions from Zend\Feed\Writer\Writer + * + * @return void + */ + protected function _loadExtensions() + { + $all = Writer::getExtensions(); + $manager = Writer::getExtensionManager(); + $exts = $all['entry']; + foreach ($exts as $ext) { + $this->extensions[$ext] = $manager->get($ext); + $this->extensions[$ext]->setEncoding($this->getEncoding()); + } + } +} diff --git a/library/Zend/Feed/Writer/Exception/BadMethodCallException.php b/library/Zend/Feed/Writer/Exception/BadMethodCallException.php new file mode 100755 index 0000000000..79d1c82c75 --- /dev/null +++ b/library/Zend/Feed/Writer/Exception/BadMethodCallException.php @@ -0,0 +1,23 @@ +container = $container; + return $this; + } + + /** + * Set feed encoding + * + * @param string $enc + * @return AbstractRenderer + */ + public function setEncoding($enc) + { + $this->encoding = $enc; + return $this; + } + + /** + * Get feed encoding + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Set DOMDocument and DOMElement on which to operate + * + * @param DOMDocument $dom + * @param DOMElement $base + * @return AbstractRenderer + */ + public function setDomDocument(DOMDocument $dom, DOMElement $base) + { + $this->dom = $dom; + $this->base = $base; + return $this; + } + + /** + * Get data container being rendered + * + * @return mixed + */ + public function getDataContainer() + { + return $this->container; + } + + /** + * Set feed type + * + * @param string $type + * @return AbstractRenderer + */ + public function setType($type) + { + $this->type = $type; + return $this; + } + + /** + * Get feedtype + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Set root element of document + * + * @param DOMElement $root + * @return AbstractRenderer + */ + public function setRootElement(DOMElement $root) + { + $this->rootElement = $root; + return $this; + } + + /** + * Get root element + * + * @return DOMElement + */ + public function getRootElement() + { + return $this->rootElement; + } + + /** + * Append namespaces to feed + * + * @return void + */ + abstract protected function _appendNamespaces(); +} diff --git a/library/Zend/Feed/Writer/Extension/Atom/Renderer/Feed.php b/library/Zend/Feed/Writer/Extension/Atom/Renderer/Feed.php new file mode 100755 index 0000000000..6b24ec01c8 --- /dev/null +++ b/library/Zend/Feed/Writer/Extension/Atom/Renderer/Feed.php @@ -0,0 +1,108 @@ +getType()) == 'atom') { + return; + } + $this->_setFeedLinks($this->dom, $this->base); + $this->_setHubs($this->dom, $this->base); + if ($this->called) { + $this->_appendNamespaces(); + } + } + + /** + * Append namespaces to root element of feed + * + * @return void + */ + protected function _appendNamespaces() + { + $this->getRootElement()->setAttribute('xmlns:atom', + 'http://www.w3.org/2005/Atom'); + } + + /** + * Set feed link elements + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setFeedLinks(DOMDocument $dom, DOMElement $root) + { + $flinks = $this->getDataContainer()->getFeedLinks(); + if (!$flinks || empty($flinks)) { + return; + } + foreach ($flinks as $type => $href) { + if (strtolower($type) == $this->getType()) { // issue 2605 + $mime = 'application/' . strtolower($type) . '+xml'; + $flink = $dom->createElement('atom:link'); + $root->appendChild($flink); + $flink->setAttribute('rel', 'self'); + $flink->setAttribute('type', $mime); + $flink->setAttribute('href', $href); + } + } + $this->called = true; + } + + /** + * Set PuSH hubs + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setHubs(DOMDocument $dom, DOMElement $root) + { + $hubs = $this->getDataContainer()->getHubs(); + if (!$hubs || empty($hubs)) { + return; + } + foreach ($hubs as $hubUrl) { + $hub = $dom->createElement('atom:link'); + $hub->setAttribute('rel', 'hub'); + $hub->setAttribute('href', $hubUrl); + $root->appendChild($hub); + } + $this->called = true; + } +} diff --git a/library/Zend/Feed/Writer/Extension/Content/Renderer/Entry.php b/library/Zend/Feed/Writer/Extension/Content/Renderer/Entry.php new file mode 100755 index 0000000000..b90315169d --- /dev/null +++ b/library/Zend/Feed/Writer/Extension/Content/Renderer/Entry.php @@ -0,0 +1,75 @@ +getType()) == 'atom') { + return; + } + $this->_setContent($this->dom, $this->base); + if ($this->called) { + $this->_appendNamespaces(); + } + } + + /** + * Append namespaces to root element + * + * @return void + */ + protected function _appendNamespaces() + { + $this->getRootElement()->setAttribute('xmlns:content', + 'http://purl.org/rss/1.0/modules/content/'); + } + + /** + * Set entry content + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setContent(DOMDocument $dom, DOMElement $root) + { + $content = $this->getDataContainer()->getContent(); + if (!$content) { + return; + } + $element = $dom->createElement('content:encoded'); + $root->appendChild($element); + $cdata = $dom->createCDATASection($content); + $element->appendChild($cdata); + $this->called = true; + } +} diff --git a/library/Zend/Feed/Writer/Extension/DublinCore/Renderer/Entry.php b/library/Zend/Feed/Writer/Extension/DublinCore/Renderer/Entry.php new file mode 100755 index 0000000000..8f9465c70d --- /dev/null +++ b/library/Zend/Feed/Writer/Extension/DublinCore/Renderer/Entry.php @@ -0,0 +1,79 @@ +getType()) == 'atom') { + return; + } + $this->_setAuthors($this->dom, $this->base); + if ($this->called) { + $this->_appendNamespaces(); + } + } + + /** + * Append namespaces to entry + * + * @return void + */ + protected function _appendNamespaces() + { + $this->getRootElement()->setAttribute('xmlns:dc', + 'http://purl.org/dc/elements/1.1/'); + } + + /** + * Set entry author elements + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setAuthors(DOMDocument $dom, DOMElement $root) + { + $authors = $this->getDataContainer()->getAuthors(); + if (!$authors || empty($authors)) { + return; + } + foreach ($authors as $data) { + $author = $this->dom->createElement('dc:creator'); + if (array_key_exists('name', $data)) { + $text = $dom->createTextNode($data['name']); + $author->appendChild($text); + $root->appendChild($author); + } + } + $this->called = true; + } +} diff --git a/library/Zend/Feed/Writer/Extension/DublinCore/Renderer/Feed.php b/library/Zend/Feed/Writer/Extension/DublinCore/Renderer/Feed.php new file mode 100755 index 0000000000..6e68f98e83 --- /dev/null +++ b/library/Zend/Feed/Writer/Extension/DublinCore/Renderer/Feed.php @@ -0,0 +1,79 @@ +getType()) == 'atom') { + return; + } + $this->_setAuthors($this->dom, $this->base); + if ($this->called) { + $this->_appendNamespaces(); + } + } + + /** + * Append namespaces to feed element + * + * @return void + */ + protected function _appendNamespaces() + { + $this->getRootElement()->setAttribute('xmlns:dc', + 'http://purl.org/dc/elements/1.1/'); + } + + /** + * Set feed authors + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setAuthors(DOMDocument $dom, DOMElement $root) + { + $authors = $this->getDataContainer()->getAuthors(); + if (!$authors || empty($authors)) { + return; + } + foreach ($authors as $data) { + $author = $this->dom->createElement('dc:creator'); + if (array_key_exists('name', $data)) { + $text = $dom->createTextNode($data['name']); + $author->appendChild($text); + $root->appendChild($author); + } + } + $this->called = true; + } +} diff --git a/library/Zend/Feed/Writer/Extension/ITunes/Entry.php b/library/Zend/Feed/Writer/Extension/ITunes/Entry.php new file mode 100755 index 0000000000..1b7b64aa5b --- /dev/null +++ b/library/Zend/Feed/Writer/Extension/ITunes/Entry.php @@ -0,0 +1,246 @@ +stringWrapper = StringUtils::getWrapper($this->encoding); + } + + /** + * Set feed encoding + * + * @param string $enc + * @return Entry + */ + public function setEncoding($enc) + { + $this->stringWrapper = StringUtils::getWrapper($enc); + $this->encoding = $enc; + return $this; + } + + /** + * Get feed encoding + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Set a block value of "yes" or "no". You may also set an empty string. + * + * @param string + * @return Entry + * @throws Writer\Exception\InvalidArgumentException + */ + public function setItunesBlock($value) + { + if (!ctype_alpha($value) && strlen($value) > 0) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: "block" may only' + . ' contain alphabetic characters'); + } + + if ($this->stringWrapper->strlen($value) > 255) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: "block" may only' + . ' contain a maximum of 255 characters'); + } + $this->data['block'] = $value; + } + + /** + * Add authors to itunes entry + * + * @param array $values + * @return Entry + */ + public function addItunesAuthors(array $values) + { + foreach ($values as $value) { + $this->addItunesAuthor($value); + } + return $this; + } + + /** + * Add author to itunes entry + * + * @param string $value + * @return Entry + * @throws Writer\Exception\InvalidArgumentException + */ + public function addItunesAuthor($value) + { + if ($this->stringWrapper->strlen($value) > 255) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: any "author" may only' + . ' contain a maximum of 255 characters each'); + } + if (!isset($this->data['authors'])) { + $this->data['authors'] = array(); + } + $this->data['authors'][] = $value; + return $this; + } + + /** + * Set duration + * + * @param int $value + * @return Entry + * @throws Writer\Exception\InvalidArgumentException + */ + public function setItunesDuration($value) + { + $value = (string) $value; + if (!ctype_digit($value) + && !preg_match("/^\d+:[0-5]{1}[0-9]{1}$/", $value) + && !preg_match("/^\d+:[0-5]{1}[0-9]{1}:[0-5]{1}[0-9]{1}$/", $value) + ) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: "duration" may only' + . ' be of a specified [[HH:]MM:]SS format'); + } + $this->data['duration'] = $value; + return $this; + } + + /** + * Set "explicit" flag + * + * @param bool $value + * @return Entry + * @throws Writer\Exception\InvalidArgumentException + */ + public function setItunesExplicit($value) + { + if (!in_array($value, array('yes', 'no', 'clean'))) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: "explicit" may only' + . ' be one of "yes", "no" or "clean"'); + } + $this->data['explicit'] = $value; + return $this; + } + + /** + * Set keywords + * + * @param array $value + * @return Entry + * @throws Writer\Exception\InvalidArgumentException + */ + public function setItunesKeywords(array $value) + { + if (count($value) > 12) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: "keywords" may only' + . ' contain a maximum of 12 terms'); + } + + $concat = implode(',', $value); + if ($this->stringWrapper->strlen($concat) > 255) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: "keywords" may only' + . ' have a concatenated length of 255 chars where terms are delimited' + . ' by a comma'); + } + $this->data['keywords'] = $value; + return $this; + } + + /** + * Set subtitle + * + * @param string $value + * @return Entry + * @throws Writer\Exception\InvalidArgumentException + */ + public function setItunesSubtitle($value) + { + if ($this->stringWrapper->strlen($value) > 255) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: "subtitle" may only' + . ' contain a maximum of 255 characters'); + } + $this->data['subtitle'] = $value; + return $this; + } + + /** + * Set summary + * + * @param string $value + * @return Entry + * @throws Writer\Exception\InvalidArgumentException + */ + public function setItunesSummary($value) + { + if ($this->stringWrapper->strlen($value) > 4000) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: "summary" may only' + . ' contain a maximum of 4000 characters'); + } + $this->data['summary'] = $value; + return $this; + } + + /** + * Overloading to itunes specific setters + * + * @param string $method + * @param array $params + * @throws Writer\Exception\BadMethodCallException + * @return mixed + */ + public function __call($method, array $params) + { + $point = lcfirst(substr($method, 9)); + if (!method_exists($this, 'setItunes' . ucfirst($point)) + && !method_exists($this, 'addItunes' . ucfirst($point)) + ) { + throw new Writer\Exception\BadMethodCallException( + 'invalid method: ' . $method + ); + } + if (!array_key_exists($point, $this->data) + || empty($this->data[$point]) + ) { + return null; + } + return $this->data[$point]; + } +} diff --git a/library/Zend/Feed/Writer/Extension/ITunes/Feed.php b/library/Zend/Feed/Writer/Extension/ITunes/Feed.php new file mode 100755 index 0000000000..22c54db6e6 --- /dev/null +++ b/library/Zend/Feed/Writer/Extension/ITunes/Feed.php @@ -0,0 +1,362 @@ +stringWrapper = StringUtils::getWrapper($this->encoding); + } + + /** + * Set feed encoding + * + * @param string $enc + * @return Feed + */ + public function setEncoding($enc) + { + $this->stringWrapper = StringUtils::getWrapper($enc); + $this->encoding = $enc; + return $this; + } + + /** + * Get feed encoding + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Set a block value of "yes" or "no". You may also set an empty string. + * + * @param string + * @return Feed + * @throws Writer\Exception\InvalidArgumentException + */ + public function setItunesBlock($value) + { + if (!ctype_alpha($value) && strlen($value) > 0) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: "block" may only' + . ' contain alphabetic characters'); + } + if ($this->stringWrapper->strlen($value) > 255) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: "block" may only' + . ' contain a maximum of 255 characters'); + } + $this->data['block'] = $value; + return $this; + } + + /** + * Add feed authors + * + * @param array $values + * @return Feed + */ + public function addItunesAuthors(array $values) + { + foreach ($values as $value) { + $this->addItunesAuthor($value); + } + return $this; + } + + /** + * Add feed author + * + * @param string $value + * @return Feed + * @throws Writer\Exception\InvalidArgumentException + */ + public function addItunesAuthor($value) + { + if ($this->stringWrapper->strlen($value) > 255) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: any "author" may only' + . ' contain a maximum of 255 characters each'); + } + if (!isset($this->data['authors'])) { + $this->data['authors'] = array(); + } + $this->data['authors'][] = $value; + return $this; + } + + /** + * Set feed categories + * + * @param array $values + * @return Feed + * @throws Writer\Exception\InvalidArgumentException + */ + public function setItunesCategories(array $values) + { + if (!isset($this->data['categories'])) { + $this->data['categories'] = array(); + } + foreach ($values as $key => $value) { + if (!is_array($value)) { + if ($this->stringWrapper->strlen($value) > 255) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: any "category" may only' + . ' contain a maximum of 255 characters each'); + } + $this->data['categories'][] = $value; + } else { + if ($this->stringWrapper->strlen($key) > 255) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: any "category" may only' + . ' contain a maximum of 255 characters each'); + } + $this->data['categories'][$key] = array(); + foreach ($value as $val) { + if ($this->stringWrapper->strlen($val) > 255) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: any "category" may only' + . ' contain a maximum of 255 characters each'); + } + $this->data['categories'][$key][] = $val; + } + } + } + return $this; + } + + /** + * Set feed image (icon) + * + * @param string $value + * @return Feed + * @throws Writer\Exception\InvalidArgumentException + */ + public function setItunesImage($value) + { + if (!Uri::factory($value)->isValid()) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: "image" may only' + . ' be a valid URI/IRI'); + } + if (!in_array(substr($value, -3), array('jpg', 'png'))) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: "image" may only' + . ' use file extension "jpg" or "png" which must be the last three' + . ' characters of the URI (i.e. no query string or fragment)'); + } + $this->data['image'] = $value; + return $this; + } + + /** + * Set feed cumulative duration + * + * @param string $value + * @return Feed + * @throws Writer\Exception\InvalidArgumentException + */ + public function setItunesDuration($value) + { + $value = (string) $value; + if (!ctype_digit($value) + && !preg_match("/^\d+:[0-5]{1}[0-9]{1}$/", $value) + && !preg_match("/^\d+:[0-5]{1}[0-9]{1}:[0-5]{1}[0-9]{1}$/", $value) + ) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: "duration" may only' + . ' be of a specified [[HH:]MM:]SS format'); + } + $this->data['duration'] = $value; + return $this; + } + + /** + * Set "explicit" flag + * + * @param bool $value + * @return Feed + * @throws Writer\Exception\InvalidArgumentException + */ + public function setItunesExplicit($value) + { + if (!in_array($value, array('yes', 'no', 'clean'))) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: "explicit" may only' + . ' be one of "yes", "no" or "clean"'); + } + $this->data['explicit'] = $value; + return $this; + } + + /** + * Set feed keywords + * + * @param array $value + * @return Feed + * @throws Writer\Exception\InvalidArgumentException + */ + public function setItunesKeywords(array $value) + { + if (count($value) > 12) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: "keywords" may only' + . ' contain a maximum of 12 terms'); + } + $concat = implode(',', $value); + if ($this->stringWrapper->strlen($concat) > 255) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: "keywords" may only' + . ' have a concatenated length of 255 chars where terms are delimited' + . ' by a comma'); + } + $this->data['keywords'] = $value; + return $this; + } + + /** + * Set new feed URL + * + * @param string $value + * @return Feed + * @throws Writer\Exception\InvalidArgumentException + */ + public function setItunesNewFeedUrl($value) + { + if (!Uri::factory($value)->isValid()) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: "newFeedUrl" may only' + . ' be a valid URI/IRI'); + } + $this->data['newFeedUrl'] = $value; + return $this; + } + + /** + * Add feed owners + * + * @param array $values + * @return Feed + */ + public function addItunesOwners(array $values) + { + foreach ($values as $value) { + $this->addItunesOwner($value); + } + return $this; + } + + /** + * Add feed owner + * + * @param array $value + * @return Feed + * @throws Writer\Exception\InvalidArgumentException + */ + public function addItunesOwner(array $value) + { + if (!isset($value['name']) || !isset($value['email'])) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: any "owner" must' + . ' be an array containing keys "name" and "email"'); + } + if ($this->stringWrapper->strlen($value['name']) > 255 + || $this->stringWrapper->strlen($value['email']) > 255 + ) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: any "owner" may only' + . ' contain a maximum of 255 characters each for "name" and "email"'); + } + if (!isset($this->data['owners'])) { + $this->data['owners'] = array(); + } + $this->data['owners'][] = $value; + return $this; + } + + /** + * Set feed subtitle + * + * @param string $value + * @return Feed + * @throws Writer\Exception\InvalidArgumentException + */ + public function setItunesSubtitle($value) + { + if ($this->stringWrapper->strlen($value) > 255) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: "subtitle" may only' + . ' contain a maximum of 255 characters'); + } + $this->data['subtitle'] = $value; + return $this; + } + + /** + * Set feed summary + * + * @param string $value + * @return Feed + * @throws Writer\Exception\InvalidArgumentException + */ + public function setItunesSummary($value) + { + if ($this->stringWrapper->strlen($value) > 4000) { + throw new Writer\Exception\InvalidArgumentException('invalid parameter: "summary" may only' + . ' contain a maximum of 4000 characters'); + } + $this->data['summary'] = $value; + return $this; + } + + /** + * Overloading: proxy to internal setters + * + * @param string $method + * @param array $params + * @return mixed + * @throws Writer\Exception\BadMethodCallException + */ + public function __call($method, array $params) + { + $point = lcfirst(substr($method, 9)); + if (!method_exists($this, 'setItunes' . ucfirst($point)) + && !method_exists($this, 'addItunes' . ucfirst($point)) + ) { + throw new Writer\Exception\BadMethodCallException( + 'invalid method: ' . $method + ); + } + if (!array_key_exists($point, $this->data) || empty($this->data[$point])) { + return null; + } + return $this->data[$point]; + } +} diff --git a/library/Zend/Feed/Writer/Extension/ITunes/Renderer/Entry.php b/library/Zend/Feed/Writer/Extension/ITunes/Renderer/Entry.php new file mode 100755 index 0000000000..bc57d1daa2 --- /dev/null +++ b/library/Zend/Feed/Writer/Extension/ITunes/Renderer/Entry.php @@ -0,0 +1,200 @@ +_setAuthors($this->dom, $this->base); + $this->_setBlock($this->dom, $this->base); + $this->_setDuration($this->dom, $this->base); + $this->_setExplicit($this->dom, $this->base); + $this->_setKeywords($this->dom, $this->base); + $this->_setSubtitle($this->dom, $this->base); + $this->_setSummary($this->dom, $this->base); + if ($this->called) { + $this->_appendNamespaces(); + } + } + + /** + * Append namespaces to entry root + * + * @return void + */ + protected function _appendNamespaces() + { + $this->getRootElement()->setAttribute('xmlns:itunes', + 'http://www.itunes.com/dtds/podcast-1.0.dtd'); + } + + /** + * Set entry authors + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setAuthors(DOMDocument $dom, DOMElement $root) + { + $authors = $this->getDataContainer()->getItunesAuthors(); + if (!$authors || empty($authors)) { + return; + } + foreach ($authors as $author) { + $el = $dom->createElement('itunes:author'); + $text = $dom->createTextNode($author); + $el->appendChild($text); + $root->appendChild($el); + $this->called = true; + } + } + + /** + * Set itunes block + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setBlock(DOMDocument $dom, DOMElement $root) + { + $block = $this->getDataContainer()->getItunesBlock(); + if ($block === null) { + return; + } + $el = $dom->createElement('itunes:block'); + $text = $dom->createTextNode($block); + $el->appendChild($text); + $root->appendChild($el); + $this->called = true; + } + + /** + * Set entry duration + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setDuration(DOMDocument $dom, DOMElement $root) + { + $duration = $this->getDataContainer()->getItunesDuration(); + if (!$duration) { + return; + } + $el = $dom->createElement('itunes:duration'); + $text = $dom->createTextNode($duration); + $el->appendChild($text); + $root->appendChild($el); + $this->called = true; + } + + /** + * Set explicit flag + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setExplicit(DOMDocument $dom, DOMElement $root) + { + $explicit = $this->getDataContainer()->getItunesExplicit(); + if ($explicit === null) { + return; + } + $el = $dom->createElement('itunes:explicit'); + $text = $dom->createTextNode($explicit); + $el->appendChild($text); + $root->appendChild($el); + $this->called = true; + } + + /** + * Set entry keywords + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setKeywords(DOMDocument $dom, DOMElement $root) + { + $keywords = $this->getDataContainer()->getItunesKeywords(); + if (!$keywords || empty($keywords)) { + return; + } + $el = $dom->createElement('itunes:keywords'); + $text = $dom->createTextNode(implode(',', $keywords)); + $el->appendChild($text); + $root->appendChild($el); + $this->called = true; + } + + /** + * Set entry subtitle + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setSubtitle(DOMDocument $dom, DOMElement $root) + { + $subtitle = $this->getDataContainer()->getItunesSubtitle(); + if (!$subtitle) { + return; + } + $el = $dom->createElement('itunes:subtitle'); + $text = $dom->createTextNode($subtitle); + $el->appendChild($text); + $root->appendChild($el); + $this->called = true; + } + + /** + * Set entry summary + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setSummary(DOMDocument $dom, DOMElement $root) + { + $summary = $this->getDataContainer()->getItunesSummary(); + if (!$summary) { + return; + } + $el = $dom->createElement('itunes:summary'); + $text = $dom->createTextNode($summary); + $el->appendChild($text); + $root->appendChild($el); + $this->called = true; + } +} diff --git a/library/Zend/Feed/Writer/Extension/ITunes/Renderer/Feed.php b/library/Zend/Feed/Writer/Extension/ITunes/Renderer/Feed.php new file mode 100755 index 0000000000..e6113a22f6 --- /dev/null +++ b/library/Zend/Feed/Writer/Extension/ITunes/Renderer/Feed.php @@ -0,0 +1,303 @@ +_setAuthors($this->dom, $this->base); + $this->_setBlock($this->dom, $this->base); + $this->_setCategories($this->dom, $this->base); + $this->_setImage($this->dom, $this->base); + $this->_setDuration($this->dom, $this->base); + $this->_setExplicit($this->dom, $this->base); + $this->_setKeywords($this->dom, $this->base); + $this->_setNewFeedUrl($this->dom, $this->base); + $this->_setOwners($this->dom, $this->base); + $this->_setSubtitle($this->dom, $this->base); + $this->_setSummary($this->dom, $this->base); + if ($this->called) { + $this->_appendNamespaces(); + } + } + + /** + * Append feed namespaces + * + * @return void + */ + protected function _appendNamespaces() + { + $this->getRootElement()->setAttribute('xmlns:itunes', + 'http://www.itunes.com/dtds/podcast-1.0.dtd'); + } + + /** + * Set feed authors + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setAuthors(DOMDocument $dom, DOMElement $root) + { + $authors = $this->getDataContainer()->getItunesAuthors(); + if (!$authors || empty($authors)) { + return; + } + foreach ($authors as $author) { + $el = $dom->createElement('itunes:author'); + $text = $dom->createTextNode($author); + $el->appendChild($text); + $root->appendChild($el); + } + $this->called = true; + } + + /** + * Set feed itunes block + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setBlock(DOMDocument $dom, DOMElement $root) + { + $block = $this->getDataContainer()->getItunesBlock(); + if ($block === null) { + return; + } + $el = $dom->createElement('itunes:block'); + $text = $dom->createTextNode($block); + $el->appendChild($text); + $root->appendChild($el); + $this->called = true; + } + + /** + * Set feed categories + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setCategories(DOMDocument $dom, DOMElement $root) + { + $cats = $this->getDataContainer()->getItunesCategories(); + if (!$cats || empty($cats)) { + return; + } + foreach ($cats as $key => $cat) { + if (!is_array($cat)) { + $el = $dom->createElement('itunes:category'); + $el->setAttribute('text', $cat); + $root->appendChild($el); + } else { + $el = $dom->createElement('itunes:category'); + $el->setAttribute('text', $key); + $root->appendChild($el); + foreach ($cat as $subcat) { + $el2 = $dom->createElement('itunes:category'); + $el2->setAttribute('text', $subcat); + $el->appendChild($el2); + } + } + } + $this->called = true; + } + + /** + * Set feed image (icon) + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setImage(DOMDocument $dom, DOMElement $root) + { + $image = $this->getDataContainer()->getItunesImage(); + if (!$image) { + return; + } + $el = $dom->createElement('itunes:image'); + $el->setAttribute('href', $image); + $root->appendChild($el); + $this->called = true; + } + + /** + * Set feed cumulative duration + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setDuration(DOMDocument $dom, DOMElement $root) + { + $duration = $this->getDataContainer()->getItunesDuration(); + if (!$duration) { + return; + } + $el = $dom->createElement('itunes:duration'); + $text = $dom->createTextNode($duration); + $el->appendChild($text); + $root->appendChild($el); + $this->called = true; + } + + /** + * Set explicit flag + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setExplicit(DOMDocument $dom, DOMElement $root) + { + $explicit = $this->getDataContainer()->getItunesExplicit(); + if ($explicit === null) { + return; + } + $el = $dom->createElement('itunes:explicit'); + $text = $dom->createTextNode($explicit); + $el->appendChild($text); + $root->appendChild($el); + $this->called = true; + } + + /** + * Set feed keywords + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setKeywords(DOMDocument $dom, DOMElement $root) + { + $keywords = $this->getDataContainer()->getItunesKeywords(); + if (!$keywords || empty($keywords)) { + return; + } + $el = $dom->createElement('itunes:keywords'); + $text = $dom->createTextNode(implode(',', $keywords)); + $el->appendChild($text); + $root->appendChild($el); + $this->called = true; + } + + /** + * Set feed's new URL + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setNewFeedUrl(DOMDocument $dom, DOMElement $root) + { + $url = $this->getDataContainer()->getItunesNewFeedUrl(); + if (!$url) { + return; + } + $el = $dom->createElement('itunes:new-feed-url'); + $text = $dom->createTextNode($url); + $el->appendChild($text); + $root->appendChild($el); + $this->called = true; + } + + /** + * Set feed owners + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setOwners(DOMDocument $dom, DOMElement $root) + { + $owners = $this->getDataContainer()->getItunesOwners(); + if (!$owners || empty($owners)) { + return; + } + foreach ($owners as $owner) { + $el = $dom->createElement('itunes:owner'); + $name = $dom->createElement('itunes:name'); + $text = $dom->createTextNode($owner['name']); + $name->appendChild($text); + $email = $dom->createElement('itunes:email'); + $text = $dom->createTextNode($owner['email']); + $email->appendChild($text); + $root->appendChild($el); + $el->appendChild($name); + $el->appendChild($email); + } + $this->called = true; + } + + /** + * Set feed subtitle + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setSubtitle(DOMDocument $dom, DOMElement $root) + { + $subtitle = $this->getDataContainer()->getItunesSubtitle(); + if (!$subtitle) { + return; + } + $el = $dom->createElement('itunes:subtitle'); + $text = $dom->createTextNode($subtitle); + $el->appendChild($text); + $root->appendChild($el); + $this->called = true; + } + + /** + * Set feed summary + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setSummary(DOMDocument $dom, DOMElement $root) + { + $summary = $this->getDataContainer()->getItunesSummary(); + if (!$summary) { + return; + } + $el = $dom->createElement('itunes:summary'); + $text = $dom->createTextNode($summary); + $el->appendChild($text); + $root->appendChild($el); + $this->called = true; + } +} diff --git a/library/Zend/Feed/Writer/Extension/RendererInterface.php b/library/Zend/Feed/Writer/Extension/RendererInterface.php new file mode 100755 index 0000000000..e72346c205 --- /dev/null +++ b/library/Zend/Feed/Writer/Extension/RendererInterface.php @@ -0,0 +1,49 @@ +getType()) == 'atom') { + return; // RSS 2.0 only + } + $this->_setCommentCount($this->dom, $this->base); + if ($this->called) { + $this->_appendNamespaces(); + } + } + + /** + * Append entry namespaces + * + * @return void + */ + protected function _appendNamespaces() + { + $this->getRootElement()->setAttribute('xmlns:slash', + 'http://purl.org/rss/1.0/modules/slash/'); + } + + /** + * Set entry comment count + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setCommentCount(DOMDocument $dom, DOMElement $root) + { + $count = $this->getDataContainer()->getCommentCount(); + if (!$count) { + $count = 0; + } + $tcount = $this->dom->createElement('slash:comments'); + $tcount->nodeValue = $count; + $root->appendChild($tcount); + $this->called = true; + } +} diff --git a/library/Zend/Feed/Writer/Extension/Threading/Renderer/Entry.php b/library/Zend/Feed/Writer/Extension/Threading/Renderer/Entry.php new file mode 100755 index 0000000000..a0ea739d84 --- /dev/null +++ b/library/Zend/Feed/Writer/Extension/Threading/Renderer/Entry.php @@ -0,0 +1,128 @@ +getType()) == 'rss') { + return; // Atom 1.0 only + } + $this->_setCommentLink($this->dom, $this->base); + $this->_setCommentFeedLinks($this->dom, $this->base); + $this->_setCommentCount($this->dom, $this->base); + if ($this->called) { + $this->_appendNamespaces(); + } + } + + /** + * Append entry namespaces + * + * @return void + */ + protected function _appendNamespaces() + { + $this->getRootElement()->setAttribute('xmlns:thr', + 'http://purl.org/syndication/thread/1.0'); + } + + /** + * Set comment link + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setCommentLink(DOMDocument $dom, DOMElement $root) + { + $link = $this->getDataContainer()->getCommentLink(); + if (!$link) { + return; + } + $clink = $this->dom->createElement('link'); + $clink->setAttribute('rel', 'replies'); + $clink->setAttribute('type', 'text/html'); + $clink->setAttribute('href', $link); + $count = $this->getDataContainer()->getCommentCount(); + if ($count !== null) { + $clink->setAttribute('thr:count', $count); + } + $root->appendChild($clink); + $this->called = true; + } + + /** + * Set comment feed links + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setCommentFeedLinks(DOMDocument $dom, DOMElement $root) + { + $links = $this->getDataContainer()->getCommentFeedLinks(); + if (!$links || empty($links)) { + return; + } + foreach ($links as $link) { + $flink = $this->dom->createElement('link'); + $flink->setAttribute('rel', 'replies'); + $flink->setAttribute('type', 'application/' . $link['type'] . '+xml'); + $flink->setAttribute('href', $link['uri']); + $count = $this->getDataContainer()->getCommentCount(); + if ($count !== null) { + $flink->setAttribute('thr:count', $count); + } + $root->appendChild($flink); + $this->called = true; + } + } + + /** + * Set entry comment count + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setCommentCount(DOMDocument $dom, DOMElement $root) + { + $count = $this->getDataContainer()->getCommentCount(); + if ($count === null) { + return; + } + $tcount = $this->dom->createElement('thr:total'); + $tcount->nodeValue = $count; + $root->appendChild($tcount); + $this->called = true; + } +} diff --git a/library/Zend/Feed/Writer/Extension/WellFormedWeb/Renderer/Entry.php b/library/Zend/Feed/Writer/Extension/WellFormedWeb/Renderer/Entry.php new file mode 100755 index 0000000000..d661360a14 --- /dev/null +++ b/library/Zend/Feed/Writer/Extension/WellFormedWeb/Renderer/Entry.php @@ -0,0 +1,79 @@ +getType()) == 'atom') { + return; // RSS 2.0 only + } + $this->_setCommentFeedLinks($this->dom, $this->base); + if ($this->called) { + $this->_appendNamespaces(); + } + } + + /** + * Append entry namespaces + * + * @return void + */ + protected function _appendNamespaces() + { + $this->getRootElement()->setAttribute('xmlns:wfw', + 'http://wellformedweb.org/CommentAPI/'); + } + + /** + * Set entry comment feed links + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setCommentFeedLinks(DOMDocument $dom, DOMElement $root) + { + $links = $this->getDataContainer()->getCommentFeedLinks(); + if (!$links || empty($links)) { + return; + } + foreach ($links as $link) { + if ($link['type'] == 'rss') { + $flink = $this->dom->createElement('wfw:commentRss'); + $text = $dom->createTextNode($link['uri']); + $flink->appendChild($text); + $root->appendChild($flink); + } + } + $this->called = true; + } +} diff --git a/library/Zend/Feed/Writer/ExtensionManager.php b/library/Zend/Feed/Writer/ExtensionManager.php new file mode 100755 index 0000000000..77d49a0dc3 --- /dev/null +++ b/library/Zend/Feed/Writer/ExtensionManager.php @@ -0,0 +1,80 @@ +pluginManager = $pluginManager; + } + + /** + * Method overloading + * + * Proxy to composed ExtensionPluginManager instance. + * + * @param string $method + * @param array $args + * @return mixed + * @throws Exception\BadMethodCallException + */ + public function __call($method, $args) + { + if (!method_exists($this->pluginManager, $method)) { + throw new Exception\BadMethodCallException(sprintf( + 'Method by name of %s does not exist in %s', + $method, + __CLASS__ + )); + } + return call_user_func_array(array($this->pluginManager, $method), $args); + } + + /** + * Get the named extension + * + * @param string $name + * @return Extension\AbstractRenderer + */ + public function get($name) + { + return $this->pluginManager->get($name); + } + + /** + * Do we have the named extension? + * + * @param string $name + * @return bool + */ + public function has($name) + { + return $this->pluginManager->has($name); + } +} diff --git a/library/Zend/Feed/Writer/ExtensionManagerInterface.php b/library/Zend/Feed/Writer/ExtensionManagerInterface.php new file mode 100755 index 0000000000..0f7e023fec --- /dev/null +++ b/library/Zend/Feed/Writer/ExtensionManagerInterface.php @@ -0,0 +1,29 @@ + 'Zend\Feed\Writer\Extension\Atom\Renderer\Feed', + 'contentrendererentry' => 'Zend\Feed\Writer\Extension\Content\Renderer\Entry', + 'dublincorerendererentry' => 'Zend\Feed\Writer\Extension\DublinCore\Renderer\Entry', + 'dublincorerendererfeed' => 'Zend\Feed\Writer\Extension\DublinCore\Renderer\Feed', + 'itunesentry' => 'Zend\Feed\Writer\Extension\ITunes\Entry', + 'itunesfeed' => 'Zend\Feed\Writer\Extension\ITunes\Feed', + 'itunesrendererentry' => 'Zend\Feed\Writer\Extension\ITunes\Renderer\Entry', + 'itunesrendererfeed' => 'Zend\Feed\Writer\Extension\ITunes\Renderer\Feed', + 'slashrendererentry' => 'Zend\Feed\Writer\Extension\Slash\Renderer\Entry', + 'threadingrendererentry' => 'Zend\Feed\Writer\Extension\Threading\Renderer\Entry', + 'wellformedwebrendererentry' => 'Zend\Feed\Writer\Extension\WellFormedWeb\Renderer\Entry', + ); + + /** + * Do not share instances + * + * @var bool + */ + protected $shareByDefault = false; + + /** + * Validate the plugin + * + * Checks that the extension loaded is of a valid type. + * + * @param mixed $plugin + * @return void + * @throws Exception\InvalidArgumentException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof Extension\AbstractRenderer) { + // we're okay + return; + } + + if ('Feed' == substr(get_class($plugin), -4)) { + // we're okay + return; + } + + if ('Entry' == substr(get_class($plugin), -5)) { + // we're okay + return; + } + + throw new Exception\InvalidArgumentException(sprintf( + 'Plugin of type %s is invalid; must implement %s\Extension\RendererInterface ' + . 'or the classname must end in "Feed" or "Entry"', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/library/Zend/Feed/Writer/Feed.php b/library/Zend/Feed/Writer/Feed.php new file mode 100755 index 0000000000..36b2b3ba9b --- /dev/null +++ b/library/Zend/Feed/Writer/Feed.php @@ -0,0 +1,239 @@ +getEncoding()) { + $entry->setEncoding($this->getEncoding()); + } + $entry->setType($this->getType()); + return $entry; + } + + /** + * Appends a Zend\Feed\Writer\Deleted object representing a new entry tombstone + * to the feed data container's internal group of entries. + * + * @param Deleted $deleted + * @return void + */ + public function addTombstone(Deleted $deleted) + { + $this->entries[] = $deleted; + } + + /** + * Creates a new Zend\Feed\Writer\Deleted data container for use. This is NOT + * added to the current feed automatically, but is necessary to create a + * container with some initial values preset based on the current feed data. + * + * @return Deleted + */ + public function createTombstone() + { + $deleted = new Deleted; + if ($this->getEncoding()) { + $deleted->setEncoding($this->getEncoding()); + } + $deleted->setType($this->getType()); + return $deleted; + } + + /** + * Appends a Zend\Feed\Writer\Entry object representing a new entry/item + * the feed data container's internal group of entries. + * + * @param Entry $entry + * @return Feed + */ + public function addEntry(Entry $entry) + { + $this->entries[] = $entry; + return $this; + } + + /** + * Removes a specific indexed entry from the internal queue. Entries must be + * added to a feed container in order to be indexed. + * + * @param int $index + * @throws Exception\InvalidArgumentException + * @return Feed + */ + public function removeEntry($index) + { + if (!isset($this->entries[$index])) { + throw new Exception\InvalidArgumentException('Undefined index: ' . $index . '. Entry does not exist.'); + } + unset($this->entries[$index]); + + return $this; + } + + /** + * Retrieve a specific indexed entry from the internal queue. Entries must be + * added to a feed container in order to be indexed. + * + * @param int $index + * @throws Exception\InvalidArgumentException + */ + public function getEntry($index = 0) + { + if (isset($this->entries[$index])) { + return $this->entries[$index]; + } + throw new Exception\InvalidArgumentException('Undefined index: ' . $index . '. Entry does not exist.'); + } + + /** + * Orders all indexed entries by date, thus offering date ordered readable + * content where a parser (or Homo Sapien) ignores the generic rule that + * XML element order is irrelevant and has no intrinsic meaning. + * + * Using this method will alter the original indexation. + * + * @return Feed + */ + public function orderByDate() + { + /** + * Could do with some improvement for performance perhaps + */ + $timestamp = time(); + $entries = array(); + foreach ($this->entries as $entry) { + if ($entry->getDateModified()) { + $timestamp = (int) $entry->getDateModified()->getTimestamp(); + } elseif ($entry->getDateCreated()) { + $timestamp = (int) $entry->getDateCreated()->getTimestamp(); + } + $entries[$timestamp] = $entry; + } + krsort($entries, SORT_NUMERIC); + $this->entries = array_values($entries); + + return $this; + } + + /** + * Get the number of feed entries. + * Required by the Iterator interface. + * + * @return int + */ + public function count() + { + return count($this->entries); + } + + /** + * Return the current entry + * + * @return Entry + */ + public function current() + { + return $this->entries[$this->key()]; + } + + /** + * Return the current feed key + * + * @return mixed + */ + public function key() + { + return $this->entriesKey; + } + + /** + * Move the feed pointer forward + * + * @return void + */ + public function next() + { + ++$this->entriesKey; + } + + /** + * Reset the pointer in the feed object + * + * @return void + */ + public function rewind() + { + $this->entriesKey = 0; + } + + /** + * Check to see if the iterator is still valid + * + * @return bool + */ + public function valid() + { + return 0 <= $this->entriesKey && $this->entriesKey < $this->count(); + } + + /** + * Attempt to build and return the feed resulting from the data set + * + * @param string $type The feed type "rss" or "atom" to export as + * @param bool $ignoreExceptions + * @throws Exception\InvalidArgumentException + * @return string + */ + public function export($type, $ignoreExceptions = false) + { + $this->setType(strtolower($type)); + $type = ucfirst($this->getType()); + if ($type !== 'Rss' && $type !== 'Atom') { + throw new Exception\InvalidArgumentException('Invalid feed type specified: ' . $type . '.' + . ' Should be one of "rss" or "atom".'); + } + $renderClass = 'Zend\\Feed\\Writer\\Renderer\\Feed\\' . $type; + $renderer = new $renderClass($this); + if ($ignoreExceptions) { + $renderer->ignoreExceptions(); + } + return $renderer->render()->saveXml(); + } +} diff --git a/library/Zend/Feed/Writer/FeedFactory.php b/library/Zend/Feed/Writer/FeedFactory.php new file mode 100755 index 0000000000..15e7a3468c --- /dev/null +++ b/library/Zend/Feed/Writer/FeedFactory.php @@ -0,0 +1,127 @@ + $value) { + // Setters + $key = static::convertKey($key); + $method = 'set' . $key; + if (method_exists($feed, $method)) { + switch ($method) { + case 'setfeedlink': + if (!is_array($value)) { + // Need an array + break; + } + if (!array_key_exists('link', $value) || !array_key_exists('type', $value)) { + // Need both keys to set this correctly + break; + } + $feed->setFeedLink($value['link'], $value['type']); + break; + default: + $feed->$method($value); + break; + } + continue; + } + + // Entries + if ('entries' == $key) { + static::createEntries($value, $feed); + continue; + } + } + + return $feed; + } + + /** + * Normalize a key + * + * @param string $key + * @return string + */ + protected static function convertKey($key) + { + $key = str_replace('_', '', strtolower($key)); + return $key; + } + + /** + * Create and attach entries to a feed + * + * @param array|Traversable $entries + * @param Feed $feed + * @throws Exception\InvalidArgumentException + * @return void + */ + protected static function createEntries($entries, Feed $feed) + { + if (!is_array($entries) && !$entries instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + '%s::factory expects the "entries" value to be an array or Traversable; received "%s"', + get_called_class(), + (is_object($entries) ? get_class($entries) : gettype($entries)) + )); + } + + foreach ($entries as $data) { + if (!is_array($data) && !$data instanceof Traversable && !$data instanceof Entry) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects an array, Traversable, or Zend\Feed\Writer\Entry argument; received "%s"', + __METHOD__, + (is_object($data) ? get_class($data) : gettype($data)) + )); + } + + // Use case 1: Entry item + if ($data instanceof Entry) { + $feed->addEntry($data); + continue; + } + + // Use case 2: iterate item and populate entry + $entry = $feed->createEntry(); + foreach ($data as $key => $value) { + $key = static::convertKey($key); + $method = 'set' . $key; + if (!method_exists($entry, $method)) { + continue; + } + $entry->$method($value); + } + $feed->addEntry($entry); + } + } +} diff --git a/library/Zend/Feed/Writer/Renderer/AbstractRenderer.php b/library/Zend/Feed/Writer/Renderer/AbstractRenderer.php new file mode 100755 index 0000000000..e104501983 --- /dev/null +++ b/library/Zend/Feed/Writer/Renderer/AbstractRenderer.php @@ -0,0 +1,233 @@ +container = $container; + $this->setType($container->getType()); + $this->_loadExtensions(); + } + + /** + * Save XML to string + * + * @return string + */ + public function saveXml() + { + return $this->getDomDocument()->saveXml(); + } + + /** + * Get DOM document + * + * @return DOMDocument + */ + public function getDomDocument() + { + return $this->dom; + } + + /** + * Get document element from DOM + * + * @return DOMElement + */ + public function getElement() + { + return $this->getDomDocument()->documentElement; + } + + /** + * Get data container of items being rendered + * + * @return Writer\AbstractFeed + */ + public function getDataContainer() + { + return $this->container; + } + + /** + * Set feed encoding + * + * @param string $enc + * @return AbstractRenderer + */ + public function setEncoding($enc) + { + $this->encoding = $enc; + return $this; + } + + /** + * Get feed encoding + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Indicate whether or not to ignore exceptions + * + * @param bool $bool + * @return AbstractRenderer + * @throws Writer\Exception\InvalidArgumentException + */ + public function ignoreExceptions($bool = true) + { + if (!is_bool($bool)) { + throw new Writer\Exception\InvalidArgumentException('Invalid parameter: $bool. Should be TRUE or FALSE (defaults to TRUE if null)'); + } + $this->ignoreExceptions = $bool; + return $this; + } + + /** + * Get exception list + * + * @return array + */ + public function getExceptions() + { + return $this->exceptions; + } + + /** + * Set the current feed type being exported to "rss" or "atom". This allows + * other objects to gracefully choose whether to execute or not, depending + * on their appropriateness for the current type, e.g. renderers. + * + * @param string $type + */ + public function setType($type) + { + $this->type = $type; + } + + /** + * Retrieve the current or last feed type exported. + * + * @return string Value will be "rss" or "atom" + */ + public function getType() + { + return $this->type; + } + + /** + * Sets the absolute root element for the XML feed being generated. This + * helps simplify the appending of namespace declarations, but also ensures + * namespaces are added to the root element - not scattered across the entire + * XML file - may assist namespace unsafe parsers and looks pretty ;). + * + * @param DOMElement $root + */ + public function setRootElement(DOMElement $root) + { + $this->rootElement = $root; + } + + /** + * Retrieve the absolute root element for the XML feed being generated. + * + * @return DOMElement + */ + public function getRootElement() + { + return $this->rootElement; + } + + /** + * Load extensions from Zend\Feed\Writer\Writer + * + * @return void + */ + protected function _loadExtensions() + { + Writer\Writer::registerCoreExtensions(); + $manager = Writer\Writer::getExtensionManager(); + $all = Writer\Writer::getExtensions(); + if (stripos(get_class($this), 'entry')) { + $exts = $all['entryRenderer']; + } else { + $exts = $all['feedRenderer']; + } + foreach ($exts as $extension) { + $plugin = $manager->get($extension); + $plugin->setDataContainer($this->getDataContainer()); + $plugin->setEncoding($this->getEncoding()); + $this->extensions[$extension] = $plugin; + } + } +} diff --git a/library/Zend/Feed/Writer/Renderer/Entry/Atom.php b/library/Zend/Feed/Writer/Renderer/Entry/Atom.php new file mode 100755 index 0000000000..975aa72689 --- /dev/null +++ b/library/Zend/Feed/Writer/Renderer/Entry/Atom.php @@ -0,0 +1,424 @@ +dom = new DOMDocument('1.0', $this->container->getEncoding()); + $this->dom->formatOutput = true; + $entry = $this->dom->createElementNS(Writer\Writer::NAMESPACE_ATOM_10, 'entry'); + $this->dom->appendChild($entry); + + $this->_setSource($this->dom, $entry); + $this->_setTitle($this->dom, $entry); + $this->_setDescription($this->dom, $entry); + $this->_setDateCreated($this->dom, $entry); + $this->_setDateModified($this->dom, $entry); + $this->_setLink($this->dom, $entry); + $this->_setId($this->dom, $entry); + $this->_setAuthors($this->dom, $entry); + $this->_setEnclosure($this->dom, $entry); + $this->_setContent($this->dom, $entry); + $this->_setCategories($this->dom, $entry); + + foreach ($this->extensions as $ext) { + $ext->setType($this->getType()); + $ext->setRootElement($this->getRootElement()); + $ext->setDOMDocument($this->getDOMDocument(), $entry); + $ext->render(); + } + + return $this; + } + + /** + * Set entry title + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + * @throws Writer\Exception\InvalidArgumentException + */ + protected function _setTitle(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getTitle()) { + $message = 'Atom 1.0 entry elements MUST contain exactly one' + . ' atom:title element but a title has not been set'; + $exception = new Writer\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + $title = $dom->createElement('title'); + $root->appendChild($title); + $title->setAttribute('type', 'html'); + $cdata = $dom->createCDATASection($this->getDataContainer()->getTitle()); + $title->appendChild($cdata); + } + + /** + * Set entry description + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setDescription(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getDescription()) { + return; // unless src content or base64 + } + $subtitle = $dom->createElement('summary'); + $root->appendChild($subtitle); + $subtitle->setAttribute('type', 'html'); + $cdata = $dom->createCDATASection( + $this->getDataContainer()->getDescription() + ); + $subtitle->appendChild($cdata); + } + + /** + * Set date entry was modified + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + * @throws Writer\Exception\InvalidArgumentException + */ + protected function _setDateModified(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getDateModified()) { + $message = 'Atom 1.0 entry elements MUST contain exactly one' + . ' atom:updated element but a modification date has not been set'; + $exception = new Writer\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + + $updated = $dom->createElement('updated'); + $root->appendChild($updated); + $text = $dom->createTextNode( + $this->getDataContainer()->getDateModified()->format(DateTime::ISO8601) + ); + $updated->appendChild($text); + } + + /** + * Set date entry was created + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setDateCreated(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getDateCreated()) { + return; + } + $el = $dom->createElement('published'); + $root->appendChild($el); + $text = $dom->createTextNode( + $this->getDataContainer()->getDateCreated()->format(DateTime::ISO8601) + ); + $el->appendChild($text); + } + + /** + * Set entry authors + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setAuthors(DOMDocument $dom, DOMElement $root) + { + $authors = $this->container->getAuthors(); + if ((!$authors || empty($authors))) { + /** + * This will actually trigger an Exception at the feed level if + * a feed level author is not set. + */ + return; + } + foreach ($authors as $data) { + $author = $this->dom->createElement('author'); + $name = $this->dom->createElement('name'); + $author->appendChild($name); + $root->appendChild($author); + $text = $dom->createTextNode($data['name']); + $name->appendChild($text); + if (array_key_exists('email', $data)) { + $email = $this->dom->createElement('email'); + $author->appendChild($email); + $text = $dom->createTextNode($data['email']); + $email->appendChild($text); + } + if (array_key_exists('uri', $data)) { + $uri = $this->dom->createElement('uri'); + $author->appendChild($uri); + $text = $dom->createTextNode($data['uri']); + $uri->appendChild($text); + } + } + } + + /** + * Set entry enclosure + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setEnclosure(DOMDocument $dom, DOMElement $root) + { + $data = $this->container->getEnclosure(); + if ((!$data || empty($data))) { + return; + } + $enclosure = $this->dom->createElement('link'); + $enclosure->setAttribute('rel', 'enclosure'); + if (isset($data['type'])) { + $enclosure->setAttribute('type', $data['type']); + } + if (isset($data['length'])) { + $enclosure->setAttribute('length', $data['length']); + } + $enclosure->setAttribute('href', $data['uri']); + $root->appendChild($enclosure); + } + + protected function _setLink(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getLink()) { + return; + } + $link = $dom->createElement('link'); + $root->appendChild($link); + $link->setAttribute('rel', 'alternate'); + $link->setAttribute('type', 'text/html'); + $link->setAttribute('href', $this->getDataContainer()->getLink()); + } + + /** + * Set entry identifier + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + * @throws Writer\Exception\InvalidArgumentException + */ + protected function _setId(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getId() + && !$this->getDataContainer()->getLink()) { + $message = 'Atom 1.0 entry elements MUST contain exactly one ' + . 'atom:id element, or as an alternative, we can use the same ' + . 'value as atom:link however neither a suitable link nor an ' + . 'id have been set'; + $exception = new Writer\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + + if (!$this->getDataContainer()->getId()) { + $this->getDataContainer()->setId( + $this->getDataContainer()->getLink()); + } + if (!Uri::factory($this->getDataContainer()->getId())->isValid() + && !preg_match( + "#^urn:[a-zA-Z0-9][a-zA-Z0-9\-]{1,31}:([a-zA-Z0-9\(\)\+\,\.\:\=\@\;\$\_\!\*\-]|%[0-9a-fA-F]{2})*#", + $this->getDataContainer()->getId()) + && !$this->_validateTagUri($this->getDataContainer()->getId()) + ) { + throw new Writer\Exception\InvalidArgumentException('Atom 1.0 IDs must be a valid URI/IRI'); + } + $id = $dom->createElement('id'); + $root->appendChild($id); + $text = $dom->createTextNode($this->getDataContainer()->getId()); + $id->appendChild($text); + } + + /** + * Validate a URI using the tag scheme (RFC 4151) + * + * @param string $id + * @return bool + */ + protected function _validateTagUri($id) + { + if (preg_match('/^tag:(?P.*),(?P\d{4}-?\d{0,2}-?\d{0,2}):(?P.*)(.*:)*$/', $id, $matches)) { + $dvalid = false; + $date = $matches['date']; + $d6 = strtotime($date); + if ((strlen($date) == 4) && $date <= date('Y')) { + $dvalid = true; + } elseif ((strlen($date) == 7) && ($d6 < strtotime("now"))) { + $dvalid = true; + } elseif ((strlen($date) == 10) && ($d6 < strtotime("now"))) { + $dvalid = true; + } + $validator = new Validator\EmailAddress; + if ($validator->isValid($matches['name'])) { + $nvalid = true; + } else { + $nvalid = $validator->isValid('info@' . $matches['name']); + } + return $dvalid && $nvalid; + } + return false; + } + + /** + * Set entry content + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + * @throws Writer\Exception\InvalidArgumentException + */ + protected function _setContent(DOMDocument $dom, DOMElement $root) + { + $content = $this->getDataContainer()->getContent(); + if (!$content && !$this->getDataContainer()->getLink()) { + $message = 'Atom 1.0 entry elements MUST contain exactly one ' + . 'atom:content element, or as an alternative, at least one link ' + . 'with a rel attribute of "alternate" to indicate an alternate ' + . 'method to consume the content.'; + $exception = new Writer\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + if (!$content) { + return; + } + $element = $dom->createElement('content'); + $element->setAttribute('type', 'xhtml'); + $xhtmlElement = $this->_loadXhtml($content); + $xhtml = $dom->importNode($xhtmlElement, true); + $element->appendChild($xhtml); + $root->appendChild($element); + } + + /** + * Load a HTML string and attempt to normalise to XML + */ + protected function _loadXhtml($content) + { + if (class_exists('tidy', false)) { + $tidy = new \tidy; + $config = array( + 'output-xhtml' => true, + 'show-body-only' => true, + 'quote-nbsp' => false + ); + $encoding = str_replace('-', '', $this->getEncoding()); + $tidy->parseString($content, $config, $encoding); + $tidy->cleanRepair(); + $xhtml = (string) $tidy; + } else { + $xhtml = $content; + } + $xhtml = preg_replace(array( + "/(<[\/]?)([a-zA-Z]+)/" + ), '$1xhtml:$2', $xhtml); + $dom = new DOMDocument('1.0', $this->getEncoding()); + $dom->loadXML('' + . $xhtml . ''); + return $dom->documentElement; + } + + /** + * Set entry categories + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setCategories(DOMDocument $dom, DOMElement $root) + { + $categories = $this->getDataContainer()->getCategories(); + if (!$categories) { + return; + } + foreach ($categories as $cat) { + $category = $dom->createElement('category'); + $category->setAttribute('term', $cat['term']); + if (isset($cat['label'])) { + $category->setAttribute('label', $cat['label']); + } else { + $category->setAttribute('label', $cat['term']); + } + if (isset($cat['scheme'])) { + $category->setAttribute('scheme', $cat['scheme']); + } + $root->appendChild($category); + } + } + + /** + * Append Source element (Atom 1.0 Feed Metadata) + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setSource(DOMDocument $dom, DOMElement $root) + { + $source = $this->getDataContainer()->getSource(); + if (!$source) { + return; + } + $renderer = new Renderer\Feed\AtomSource($source); + $renderer->setType($this->getType()); + $element = $renderer->render()->getElement(); + $imported = $dom->importNode($element, true); + $root->appendChild($imported); + } +} diff --git a/library/Zend/Feed/Writer/Renderer/Entry/Atom/Deleted.php b/library/Zend/Feed/Writer/Renderer/Entry/Atom/Deleted.php new file mode 100755 index 0000000000..65ace00bd7 --- /dev/null +++ b/library/Zend/Feed/Writer/Renderer/Entry/Atom/Deleted.php @@ -0,0 +1,102 @@ +dom = new DOMDocument('1.0', $this->container->getEncoding()); + $this->dom->formatOutput = true; + $entry = $this->dom->createElement('at:deleted-entry'); + $this->dom->appendChild($entry); + + $entry->setAttribute('ref', $this->container->getReference()); + $entry->setAttribute('when', $this->container->getWhen()->format(DateTime::ISO8601)); + + $this->_setBy($this->dom, $entry); + $this->_setComment($this->dom, $entry); + + return $this; + } + + /** + * Set tombstone comment + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setComment(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getComment()) { + return; + } + $c = $dom->createElement('at:comment'); + $root->appendChild($c); + $c->setAttribute('type', 'html'); + $cdata = $dom->createCDATASection($this->getDataContainer()->getComment()); + $c->appendChild($cdata); + } + + /** + * Set entry authors + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setBy(DOMDocument $dom, DOMElement $root) + { + $data = $this->container->getBy(); + if ((!$data || empty($data))) { + return; + } + $author = $this->dom->createElement('at:by'); + $name = $this->dom->createElement('name'); + $author->appendChild($name); + $root->appendChild($author); + $text = $dom->createTextNode($data['name']); + $name->appendChild($text); + if (array_key_exists('email', $data)) { + $email = $this->dom->createElement('email'); + $author->appendChild($email); + $text = $dom->createTextNode($data['email']); + $email->appendChild($text); + } + if (array_key_exists('uri', $data)) { + $uri = $this->dom->createElement('uri'); + $author->appendChild($uri); + $text = $dom->createTextNode($data['uri']); + $uri->appendChild($text); + } + } +} diff --git a/library/Zend/Feed/Writer/Renderer/Entry/AtomDeleted.php b/library/Zend/Feed/Writer/Renderer/Entry/AtomDeleted.php new file mode 100755 index 0000000000..1ed4aa3d9e --- /dev/null +++ b/library/Zend/Feed/Writer/Renderer/Entry/AtomDeleted.php @@ -0,0 +1,104 @@ +dom = new DOMDocument('1.0', $this->container->getEncoding()); + $this->dom->formatOutput = true; + $entry = $this->dom->createElement('at:deleted-entry'); + $this->dom->appendChild($entry); + + $entry->setAttribute('ref', $this->container->getReference()); + $entry->setAttribute('when', $this->container->getWhen()->format(DateTime::ISO8601)); + + $this->_setBy($this->dom, $entry); + $this->_setComment($this->dom, $entry); + + return $this; + } + + /** + * Set tombstone comment + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setComment(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getComment()) { + return; + } + $c = $dom->createElement('at:comment'); + $root->appendChild($c); + $c->setAttribute('type', 'html'); + $cdata = $dom->createCDATASection($this->getDataContainer()->getComment()); + $c->appendChild($cdata); + } + + /** + * Set entry authors + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setBy(DOMDocument $dom, DOMElement $root) + { + $data = $this->container->getBy(); + if ((!$data || empty($data))) { + return; + } + $author = $this->dom->createElement('at:by'); + $name = $this->dom->createElement('name'); + $author->appendChild($name); + $root->appendChild($author); + $text = $dom->createTextNode($data['name']); + $name->appendChild($text); + if (array_key_exists('email', $data)) { + $email = $this->dom->createElement('email'); + $author->appendChild($email); + $text = $dom->createTextNode($data['email']); + $email->appendChild($text); + } + if (array_key_exists('uri', $data)) { + $uri = $this->dom->createElement('uri'); + $author->appendChild($uri); + $text = $dom->createTextNode($data['uri']); + $uri->appendChild($text); + } + } +} diff --git a/library/Zend/Feed/Writer/Renderer/Entry/Rss.php b/library/Zend/Feed/Writer/Renderer/Entry/Rss.php new file mode 100755 index 0000000000..2338cdc213 --- /dev/null +++ b/library/Zend/Feed/Writer/Renderer/Entry/Rss.php @@ -0,0 +1,329 @@ +dom = new DOMDocument('1.0', $this->container->getEncoding()); + $this->dom->formatOutput = true; + $this->dom->substituteEntities = false; + $entry = $this->dom->createElement('item'); + $this->dom->appendChild($entry); + + $this->_setTitle($this->dom, $entry); + $this->_setDescription($this->dom, $entry); + $this->_setDateCreated($this->dom, $entry); + $this->_setDateModified($this->dom, $entry); + $this->_setLink($this->dom, $entry); + $this->_setId($this->dom, $entry); + $this->_setAuthors($this->dom, $entry); + $this->_setEnclosure($this->dom, $entry); + $this->_setCommentLink($this->dom, $entry); + $this->_setCategories($this->dom, $entry); + foreach ($this->extensions as $ext) { + $ext->setType($this->getType()); + $ext->setRootElement($this->getRootElement()); + $ext->setDOMDocument($this->getDOMDocument(), $entry); + $ext->render(); + } + + return $this; + } + + /** + * Set entry title + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + * @throws Writer\Exception\InvalidArgumentException + */ + protected function _setTitle(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getDescription() + && !$this->getDataContainer()->getTitle()) { + $message = 'RSS 2.0 entry elements SHOULD contain exactly one' + . ' title element but a title has not been set. In addition, there' + . ' is no description as required in the absence of a title.'; + $exception = new Writer\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + $title = $dom->createElement('title'); + $root->appendChild($title); + $text = $dom->createTextNode($this->getDataContainer()->getTitle()); + $title->appendChild($text); + } + + /** + * Set entry description + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + * @throws Writer\Exception\InvalidArgumentException + */ + protected function _setDescription(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getDescription() + && !$this->getDataContainer()->getTitle()) { + $message = 'RSS 2.0 entry elements SHOULD contain exactly one' + . ' description element but a description has not been set. In' + . ' addition, there is no title element as required in the absence' + . ' of a description.'; + $exception = new Writer\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + if (!$this->getDataContainer()->getDescription()) { + return; + } + $subtitle = $dom->createElement('description'); + $root->appendChild($subtitle); + $text = $dom->createCDATASection($this->getDataContainer()->getDescription()); + $subtitle->appendChild($text); + } + + /** + * Set date entry was last modified + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setDateModified(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getDateModified()) { + return; + } + + $updated = $dom->createElement('pubDate'); + $root->appendChild($updated); + $text = $dom->createTextNode( + $this->getDataContainer()->getDateModified()->format(DateTime::RSS) + ); + $updated->appendChild($text); + } + + /** + * Set date entry was created + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setDateCreated(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getDateCreated()) { + return; + } + if (!$this->getDataContainer()->getDateModified()) { + $this->getDataContainer()->setDateModified( + $this->getDataContainer()->getDateCreated() + ); + } + } + + /** + * Set entry authors + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setAuthors(DOMDocument $dom, DOMElement $root) + { + $authors = $this->container->getAuthors(); + if ((!$authors || empty($authors))) { + return; + } + foreach ($authors as $data) { + $author = $this->dom->createElement('author'); + $name = $data['name']; + if (array_key_exists('email', $data)) { + $name = $data['email'] . ' (' . $data['name'] . ')'; + } + $text = $dom->createTextNode($name); + $author->appendChild($text); + $root->appendChild($author); + } + } + + /** + * Set entry enclosure + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + * @throws Writer\Exception\InvalidArgumentException + */ + protected function _setEnclosure(DOMDocument $dom, DOMElement $root) + { + $data = $this->container->getEnclosure(); + if ((!$data || empty($data))) { + return; + } + if (!isset($data['type'])) { + $exception = new Writer\Exception\InvalidArgumentException('Enclosure "type" is not set'); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + if (!isset($data['length'])) { + $exception = new Writer\Exception\InvalidArgumentException('Enclosure "length" is not set'); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + if (isset($data['length']) && (int) $data['length'] <= 0) { + $exception = new Writer\Exception\InvalidArgumentException('Enclosure "length" must be an integer' + . ' indicating the content\'s length in bytes'); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + $enclosure = $this->dom->createElement('enclosure'); + $enclosure->setAttribute('type', $data['type']); + $enclosure->setAttribute('length', $data['length']); + $enclosure->setAttribute('url', $data['uri']); + $root->appendChild($enclosure); + } + + /** + * Set link to entry + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setLink(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getLink()) { + return; + } + $link = $dom->createElement('link'); + $root->appendChild($link); + $text = $dom->createTextNode($this->getDataContainer()->getLink()); + $link->appendChild($text); + } + + /** + * Set entry identifier + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setId(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getId() + && !$this->getDataContainer()->getLink()) { + return; + } + + $id = $dom->createElement('guid'); + $root->appendChild($id); + if (!$this->getDataContainer()->getId()) { + $this->getDataContainer()->setId( + $this->getDataContainer()->getLink()); + } + $text = $dom->createTextNode($this->getDataContainer()->getId()); + $id->appendChild($text); + if (!Uri::factory($this->getDataContainer()->getId())->isValid()) { + $id->setAttribute('isPermaLink', 'false'); + } + } + + /** + * Set link to entry comments + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setCommentLink(DOMDocument $dom, DOMElement $root) + { + $link = $this->getDataContainer()->getCommentLink(); + if (!$link) { + return; + } + $clink = $this->dom->createElement('comments'); + $text = $dom->createTextNode($link); + $clink->appendChild($text); + $root->appendChild($clink); + } + + /** + * Set entry categories + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setCategories(DOMDocument $dom, DOMElement $root) + { + $categories = $this->getDataContainer()->getCategories(); + if (!$categories) { + return; + } + foreach ($categories as $cat) { + $category = $dom->createElement('category'); + if (isset($cat['scheme'])) { + $category->setAttribute('domain', $cat['scheme']); + } + $text = $dom->createCDATASection($cat['term']); + $category->appendChild($text); + $root->appendChild($category); + } + } +} diff --git a/library/Zend/Feed/Writer/Renderer/Feed/AbstractAtom.php b/library/Zend/Feed/Writer/Renderer/Feed/AbstractAtom.php new file mode 100755 index 0000000000..e7ad9f56ba --- /dev/null +++ b/library/Zend/Feed/Writer/Renderer/Feed/AbstractAtom.php @@ -0,0 +1,403 @@ +getDataContainer()->getLanguage()) { + $root->setAttribute('xml:lang', $this->getDataContainer() + ->getLanguage()); + } + } + + /** + * Set feed title + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + * @throws Writer\Exception\InvalidArgumentException + */ + protected function _setTitle(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getTitle()) { + $message = 'Atom 1.0 feed elements MUST contain exactly one' + . ' atom:title element but a title has not been set'; + $exception = new Writer\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + + $title = $dom->createElement('title'); + $root->appendChild($title); + $title->setAttribute('type', 'text'); + $text = $dom->createTextNode($this->getDataContainer()->getTitle()); + $title->appendChild($text); + } + + /** + * Set feed description + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setDescription(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getDescription()) { + return; + } + $subtitle = $dom->createElement('subtitle'); + $root->appendChild($subtitle); + $subtitle->setAttribute('type', 'text'); + $text = $dom->createTextNode($this->getDataContainer()->getDescription()); + $subtitle->appendChild($text); + } + + /** + * Set date feed was last modified + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + * @throws Writer\Exception\InvalidArgumentException + */ + protected function _setDateModified(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getDateModified()) { + $message = 'Atom 1.0 feed elements MUST contain exactly one' + . ' atom:updated element but a modification date has not been set'; + $exception = new Writer\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + + $updated = $dom->createElement('updated'); + $root->appendChild($updated); + $text = $dom->createTextNode( + $this->getDataContainer()->getDateModified()->format(DateTime::ISO8601) + ); + $updated->appendChild($text); + } + + /** + * Set feed generator string + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setGenerator(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getGenerator()) { + $this->getDataContainer()->setGenerator('Zend_Feed_Writer', + Version::VERSION, 'http://framework.zend.com'); + } + + $gdata = $this->getDataContainer()->getGenerator(); + $generator = $dom->createElement('generator'); + $root->appendChild($generator); + $text = $dom->createTextNode($gdata['name']); + $generator->appendChild($text); + if (array_key_exists('uri', $gdata)) { + $generator->setAttribute('uri', $gdata['uri']); + } + if (array_key_exists('version', $gdata)) { + $generator->setAttribute('version', $gdata['version']); + } + } + + /** + * Set link to feed + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setLink(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getLink()) { + return; + } + $link = $dom->createElement('link'); + $root->appendChild($link); + $link->setAttribute('rel', 'alternate'); + $link->setAttribute('type', 'text/html'); + $link->setAttribute('href', $this->getDataContainer()->getLink()); + } + + /** + * Set feed links + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + * @throws Writer\Exception\InvalidArgumentException + */ + protected function _setFeedLinks(DOMDocument $dom, DOMElement $root) + { + $flinks = $this->getDataContainer()->getFeedLinks(); + if (!$flinks || !array_key_exists('atom', $flinks)) { + $message = 'Atom 1.0 feed elements SHOULD contain one atom:link ' + . 'element with a rel attribute value of "self". This is the ' + . 'preferred URI for retrieving Atom Feed Documents representing ' + . 'this Atom feed but a feed link has not been set'; + $exception = new Writer\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + + foreach ($flinks as $type => $href) { + $mime = 'application/' . strtolower($type) . '+xml'; + $flink = $dom->createElement('link'); + $root->appendChild($flink); + $flink->setAttribute('rel', 'self'); + $flink->setAttribute('type', $mime); + $flink->setAttribute('href', $href); + } + } + + /** + * Set feed authors + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setAuthors(DOMDocument $dom, DOMElement $root) + { + $authors = $this->container->getAuthors(); + if (!$authors || empty($authors)) { + /** + * Technically we should defer an exception until we can check + * that all entries contain an author. If any entry is missing + * an author, then a missing feed author element is invalid + */ + return; + } + foreach ($authors as $data) { + $author = $this->dom->createElement('author'); + $name = $this->dom->createElement('name'); + $author->appendChild($name); + $root->appendChild($author); + $text = $dom->createTextNode($data['name']); + $name->appendChild($text); + if (array_key_exists('email', $data)) { + $email = $this->dom->createElement('email'); + $author->appendChild($email); + $text = $dom->createTextNode($data['email']); + $email->appendChild($text); + } + if (array_key_exists('uri', $data)) { + $uri = $this->dom->createElement('uri'); + $author->appendChild($uri); + $text = $dom->createTextNode($data['uri']); + $uri->appendChild($text); + } + } + } + + /** + * Set feed identifier + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + * @throws Writer\Exception\InvalidArgumentException + */ + protected function _setId(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getId() + && !$this->getDataContainer()->getLink()) { + $message = 'Atom 1.0 feed elements MUST contain exactly one ' + . 'atom:id element, or as an alternative, we can use the same ' + . 'value as atom:link however neither a suitable link nor an ' + . 'id have been set'; + $exception = new Writer\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + + if (!$this->getDataContainer()->getId()) { + $this->getDataContainer()->setId( + $this->getDataContainer()->getLink()); + } + $id = $dom->createElement('id'); + $root->appendChild($id); + $text = $dom->createTextNode($this->getDataContainer()->getId()); + $id->appendChild($text); + } + + /** + * Set feed copyright + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setCopyright(DOMDocument $dom, DOMElement $root) + { + $copyright = $this->getDataContainer()->getCopyright(); + if (!$copyright) { + return; + } + $copy = $dom->createElement('rights'); + $root->appendChild($copy); + $text = $dom->createTextNode($copyright); + $copy->appendChild($text); + } + + /** + * Set feed level logo (image) + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setImage(DOMDocument $dom, DOMElement $root) + { + $image = $this->getDataContainer()->getImage(); + if (!$image) { + return; + } + $img = $dom->createElement('logo'); + $root->appendChild($img); + $text = $dom->createTextNode($image['uri']); + $img->appendChild($text); + } + + /** + * Set date feed was created + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setDateCreated(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getDateCreated()) { + return; + } + if (!$this->getDataContainer()->getDateModified()) { + $this->getDataContainer()->setDateModified( + $this->getDataContainer()->getDateCreated() + ); + } + } + + /** + * Set base URL to feed links + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setBaseUrl(DOMDocument $dom, DOMElement $root) + { + $baseUrl = $this->getDataContainer()->getBaseUrl(); + if (!$baseUrl) { + return; + } + $root->setAttribute('xml:base', $baseUrl); + } + + /** + * Set hubs to which this feed pushes + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setHubs(DOMDocument $dom, DOMElement $root) + { + $hubs = $this->getDataContainer()->getHubs(); + if (!$hubs) { + return; + } + foreach ($hubs as $hubUrl) { + $hub = $dom->createElement('link'); + $hub->setAttribute('rel', 'hub'); + $hub->setAttribute('href', $hubUrl); + $root->appendChild($hub); + } + } + + /** + * Set feed categories + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setCategories(DOMDocument $dom, DOMElement $root) + { + $categories = $this->getDataContainer()->getCategories(); + if (!$categories) { + return; + } + foreach ($categories as $cat) { + $category = $dom->createElement('category'); + $category->setAttribute('term', $cat['term']); + if (isset($cat['label'])) { + $category->setAttribute('label', $cat['label']); + } else { + $category->setAttribute('label', $cat['term']); + } + if (isset($cat['scheme'])) { + $category->setAttribute('scheme', $cat['scheme']); + } + $root->appendChild($category); + } + } +} diff --git a/library/Zend/Feed/Writer/Renderer/Feed/Atom.php b/library/Zend/Feed/Writer/Renderer/Feed/Atom.php new file mode 100755 index 0000000000..87b6b94ac0 --- /dev/null +++ b/library/Zend/Feed/Writer/Renderer/Feed/Atom.php @@ -0,0 +1,96 @@ +container->getEncoding()) { + $this->container->setEncoding('UTF-8'); + } + $this->dom = new DOMDocument('1.0', $this->container->getEncoding()); + $this->dom->formatOutput = true; + $root = $this->dom->createElementNS( + Writer\Writer::NAMESPACE_ATOM_10, 'feed' + ); + $this->setRootElement($root); + $this->dom->appendChild($root); + $this->_setLanguage($this->dom, $root); + $this->_setBaseUrl($this->dom, $root); + $this->_setTitle($this->dom, $root); + $this->_setDescription($this->dom, $root); + $this->_setImage($this->dom, $root); + $this->_setDateCreated($this->dom, $root); + $this->_setDateModified($this->dom, $root); + $this->_setGenerator($this->dom, $root); + $this->_setLink($this->dom, $root); + $this->_setFeedLinks($this->dom, $root); + $this->_setId($this->dom, $root); + $this->_setAuthors($this->dom, $root); + $this->_setCopyright($this->dom, $root); + $this->_setCategories($this->dom, $root); + $this->_setHubs($this->dom, $root); + + foreach ($this->extensions as $ext) { + $ext->setType($this->getType()); + $ext->setRootElement($this->getRootElement()); + $ext->setDOMDocument($this->getDOMDocument(), $root); + $ext->render(); + } + + foreach ($this->container as $entry) { + if ($this->getDataContainer()->getEncoding()) { + $entry->setEncoding($this->getDataContainer()->getEncoding()); + } + if ($entry instanceof Writer\Entry) { + $renderer = new Renderer\Entry\Atom($entry); + } else { + if (!$this->dom->documentElement->hasAttribute('xmlns:at')) { + $this->dom->documentElement->setAttribute( + 'xmlns:at', 'http://purl.org/atompub/tombstones/1.0' + ); + } + $renderer = new Renderer\Entry\AtomDeleted($entry); + } + if ($this->ignoreExceptions === true) { + $renderer->ignoreExceptions(); + } + $renderer->setType($this->getType()); + $renderer->setRootElement($this->dom->documentElement); + $renderer->render(); + $element = $renderer->getElement(); + $imported = $this->dom->importNode($element, true); + $root->appendChild($imported); + } + return $this; + } +} diff --git a/library/Zend/Feed/Writer/Renderer/Feed/Atom/AbstractAtom.php b/library/Zend/Feed/Writer/Renderer/Feed/Atom/AbstractAtom.php new file mode 100755 index 0000000000..379cd5c9f7 --- /dev/null +++ b/library/Zend/Feed/Writer/Renderer/Feed/Atom/AbstractAtom.php @@ -0,0 +1,400 @@ +getDataContainer()->getLanguage()) { + $root->setAttribute('xml:lang', $this->getDataContainer() + ->getLanguage()); + } + } + + /** + * Set feed title + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + * @throws Feed\Exception\InvalidArgumentException + */ + protected function _setTitle(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getTitle()) { + $message = 'Atom 1.0 feed elements MUST contain exactly one' + . ' atom:title element but a title has not been set'; + $exception = new Feed\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + + $title = $dom->createElement('title'); + $root->appendChild($title); + $title->setAttribute('type', 'text'); + $text = $dom->createTextNode($this->getDataContainer()->getTitle()); + $title->appendChild($text); + } + + /** + * Set feed description + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setDescription(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getDescription()) { + return; + } + $subtitle = $dom->createElement('subtitle'); + $root->appendChild($subtitle); + $subtitle->setAttribute('type', 'text'); + $text = $dom->createTextNode($this->getDataContainer()->getDescription()); + $subtitle->appendChild($text); + } + + /** + * Set date feed was last modified + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + * @throws Feed\Exception\InvalidArgumentException + */ + protected function _setDateModified(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getDateModified()) { + $message = 'Atom 1.0 feed elements MUST contain exactly one' + . ' atom:updated element but a modification date has not been set'; + $exception = new Feed\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + + $updated = $dom->createElement('updated'); + $root->appendChild($updated); + $text = $dom->createTextNode( + $this->getDataContainer()->getDateModified()->format(DateTime::ISO8601) + ); + $updated->appendChild($text); + } + + /** + * Set feed generator string + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setGenerator(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getGenerator()) { + $this->getDataContainer()->setGenerator('Zend_Feed_Writer', + Version::VERSION, 'http://framework.zend.com'); + } + + $gdata = $this->getDataContainer()->getGenerator(); + $generator = $dom->createElement('generator'); + $root->appendChild($generator); + $text = $dom->createTextNode($gdata['name']); + $generator->appendChild($text); + if (array_key_exists('uri', $gdata)) { + $generator->setAttribute('uri', $gdata['uri']); + } + if (array_key_exists('version', $gdata)) { + $generator->setAttribute('version', $gdata['version']); + } + } + + /** + * Set link to feed + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setLink(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getLink()) { + return; + } + $link = $dom->createElement('link'); + $root->appendChild($link); + $link->setAttribute('rel', 'alternate'); + $link->setAttribute('type', 'text/html'); + $link->setAttribute('href', $this->getDataContainer()->getLink()); + } + + /** + * Set feed links + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + * @throws Feed\Exception\InvalidArgumentException + */ + protected function _setFeedLinks(DOMDocument $dom, DOMElement $root) + { + $flinks = $this->getDataContainer()->getFeedLinks(); + if (!$flinks || !array_key_exists('atom', $flinks)) { + $message = 'Atom 1.0 feed elements SHOULD contain one atom:link ' + . 'element with a rel attribute value of "self". This is the ' + . 'preferred URI for retrieving Atom Feed Documents representing ' + . 'this Atom feed but a feed link has not been set'; + $exception = new Feed\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + + foreach ($flinks as $type => $href) { + $mime = 'application/' . strtolower($type) . '+xml'; + $flink = $dom->createElement('link'); + $root->appendChild($flink); + $flink->setAttribute('rel', 'self'); + $flink->setAttribute('type', $mime); + $flink->setAttribute('href', $href); + } + } + + /** + * Set feed authors + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setAuthors(DOMDocument $dom, DOMElement $root) + { + $authors = $this->container->getAuthors(); + if (!$authors || empty($authors)) { + /** + * Technically we should defer an exception until we can check + * that all entries contain an author. If any entry is missing + * an author, then a missing feed author element is invalid + */ + return; + } + foreach ($authors as $data) { + $author = $this->dom->createElement('author'); + $name = $this->dom->createElement('name'); + $author->appendChild($name); + $root->appendChild($author); + $text = $dom->createTextNode($data['name']); + $name->appendChild($text); + if (array_key_exists('email', $data)) { + $email = $this->dom->createElement('email'); + $author->appendChild($email); + $text = $dom->createTextNode($data['email']); + $email->appendChild($text); + } + if (array_key_exists('uri', $data)) { + $uri = $this->dom->createElement('uri'); + $author->appendChild($uri); + $text = $dom->createTextNode($data['uri']); + $uri->appendChild($text); + } + } + } + + /** + * Set feed identifier + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + * @throws Feed\Exception\InvalidArgumentException + */ + protected function _setId(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getId() + && !$this->getDataContainer()->getLink()) { + $message = 'Atom 1.0 feed elements MUST contain exactly one ' + . 'atom:id element, or as an alternative, we can use the same ' + . 'value as atom:link however neither a suitable link nor an ' + . 'id have been set'; + $exception = new Feed\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + + if (!$this->getDataContainer()->getId()) { + $this->getDataContainer()->setId( + $this->getDataContainer()->getLink()); + } + $id = $dom->createElement('id'); + $root->appendChild($id); + $text = $dom->createTextNode($this->getDataContainer()->getId()); + $id->appendChild($text); + } + + /** + * Set feed copyright + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setCopyright(DOMDocument $dom, DOMElement $root) + { + $copyright = $this->getDataContainer()->getCopyright(); + if (!$copyright) { + return; + } + $copy = $dom->createElement('rights'); + $root->appendChild($copy); + $text = $dom->createTextNode($copyright); + $copy->appendChild($text); + } + /** + * Set feed level logo (image) + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setImage(DOMDocument $dom, DOMElement $root) + { + $image = $this->getDataContainer()->getImage(); + if (!$image) { + return; + } + $img = $dom->createElement('logo'); + $root->appendChild($img); + $text = $dom->createTextNode($image['uri']); + $img->appendChild($text); + } + + + /** + * Set date feed was created + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setDateCreated(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getDateCreated()) { + return; + } + if (!$this->getDataContainer()->getDateModified()) { + $this->getDataContainer()->setDateModified( + $this->getDataContainer()->getDateCreated() + ); + } + } + + /** + * Set base URL to feed links + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setBaseUrl(DOMDocument $dom, DOMElement $root) + { + $baseUrl = $this->getDataContainer()->getBaseUrl(); + if (!$baseUrl) { + return; + } + $root->setAttribute('xml:base', $baseUrl); + } + + /** + * Set hubs to which this feed pushes + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setHubs(DOMDocument $dom, DOMElement $root) + { + $hubs = $this->getDataContainer()->getHubs(); + if (!$hubs) { + return; + } + foreach ($hubs as $hubUrl) { + $hub = $dom->createElement('link'); + $hub->setAttribute('rel', 'hub'); + $hub->setAttribute('href', $hubUrl); + $root->appendChild($hub); + } + } + + /** + * Set feed categories + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setCategories(DOMDocument $dom, DOMElement $root) + { + $categories = $this->getDataContainer()->getCategories(); + if (!$categories) { + return; + } + foreach ($categories as $cat) { + $category = $dom->createElement('category'); + $category->setAttribute('term', $cat['term']); + if (isset($cat['label'])) { + $category->setAttribute('label', $cat['label']); + } else { + $category->setAttribute('label', $cat['term']); + } + if (isset($cat['scheme'])) { + $category->setAttribute('scheme', $cat['scheme']); + } + $root->appendChild($category); + } + } +} diff --git a/library/Zend/Feed/Writer/Renderer/Feed/Atom/Source.php b/library/Zend/Feed/Writer/Renderer/Feed/Atom/Source.php new file mode 100755 index 0000000000..30f576a1f5 --- /dev/null +++ b/library/Zend/Feed/Writer/Renderer/Feed/Atom/Source.php @@ -0,0 +1,92 @@ +container->getEncoding()) { + $this->container->setEncoding('UTF-8'); + } + $this->dom = new DOMDocument('1.0', $this->container->getEncoding()); + $this->dom->formatOutput = true; + $root = $this->dom->createElement('source'); + $this->setRootElement($root); + $this->dom->appendChild($root); + $this->_setLanguage($this->dom, $root); + $this->_setBaseUrl($this->dom, $root); + $this->_setTitle($this->dom, $root); + $this->_setDescription($this->dom, $root); + $this->_setDateCreated($this->dom, $root); + $this->_setDateModified($this->dom, $root); + $this->_setGenerator($this->dom, $root); + $this->_setLink($this->dom, $root); + $this->_setFeedLinks($this->dom, $root); + $this->_setId($this->dom, $root); + $this->_setAuthors($this->dom, $root); + $this->_setCopyright($this->dom, $root); + $this->_setCategories($this->dom, $root); + + foreach ($this->extensions as $ext) { + $ext->setType($this->getType()); + $ext->setRootElement($this->getRootElement()); + $ext->setDomDocument($this->getDomDocument(), $root); + $ext->render(); + } + return $this; + } + + /** + * Set feed generator string + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setGenerator(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getGenerator()) { + return; + } + + $gdata = $this->getDataContainer()->getGenerator(); + $generator = $dom->createElement('generator'); + $root->appendChild($generator); + $text = $dom->createTextNode($gdata['name']); + $generator->appendChild($text); + if (array_key_exists('uri', $gdata)) { + $generator->setAttribute('uri', $gdata['uri']); + } + if (array_key_exists('version', $gdata)) { + $generator->setAttribute('version', $gdata['version']); + } + } +} diff --git a/library/Zend/Feed/Writer/Renderer/Feed/AtomSource.php b/library/Zend/Feed/Writer/Renderer/Feed/AtomSource.php new file mode 100755 index 0000000000..bdadd4db02 --- /dev/null +++ b/library/Zend/Feed/Writer/Renderer/Feed/AtomSource.php @@ -0,0 +1,94 @@ +container->getEncoding()) { + $this->container->setEncoding('UTF-8'); + } + $this->dom = new DOMDocument('1.0', $this->container->getEncoding()); + $this->dom->formatOutput = true; + $root = $this->dom->createElement('source'); + $this->setRootElement($root); + $this->dom->appendChild($root); + $this->_setLanguage($this->dom, $root); + $this->_setBaseUrl($this->dom, $root); + $this->_setTitle($this->dom, $root); + $this->_setDescription($this->dom, $root); + $this->_setDateCreated($this->dom, $root); + $this->_setDateModified($this->dom, $root); + $this->_setGenerator($this->dom, $root); + $this->_setLink($this->dom, $root); + $this->_setFeedLinks($this->dom, $root); + $this->_setId($this->dom, $root); + $this->_setAuthors($this->dom, $root); + $this->_setCopyright($this->dom, $root); + $this->_setCategories($this->dom, $root); + + foreach ($this->extensions as $ext) { + $ext->setType($this->getType()); + $ext->setRootElement($this->getRootElement()); + $ext->setDOMDocument($this->getDOMDocument(), $root); + $ext->render(); + } + return $this; + } + + /** + * Set feed generator string + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setGenerator(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getGenerator()) { + return; + } + + $gdata = $this->getDataContainer()->getGenerator(); + $generator = $dom->createElement('generator'); + $root->appendChild($generator); + $text = $dom->createTextNode($gdata['name']); + $generator->appendChild($text); + if (array_key_exists('uri', $gdata)) { + $generator->setAttribute('uri', $gdata['uri']); + } + if (array_key_exists('version', $gdata)) { + $generator->setAttribute('version', $gdata['version']); + } + } +} diff --git a/library/Zend/Feed/Writer/Renderer/Feed/Rss.php b/library/Zend/Feed/Writer/Renderer/Feed/Rss.php new file mode 100755 index 0000000000..75c502e323 --- /dev/null +++ b/library/Zend/Feed/Writer/Renderer/Feed/Rss.php @@ -0,0 +1,484 @@ +dom = new DOMDocument('1.0', $this->container->getEncoding()); + $this->dom->formatOutput = true; + $this->dom->substituteEntities = false; + $rss = $this->dom->createElement('rss'); + $this->setRootElement($rss); + $rss->setAttribute('version', '2.0'); + + $channel = $this->dom->createElement('channel'); + $rss->appendChild($channel); + $this->dom->appendChild($rss); + $this->_setLanguage($this->dom, $channel); + $this->_setBaseUrl($this->dom, $channel); + $this->_setTitle($this->dom, $channel); + $this->_setDescription($this->dom, $channel); + $this->_setImage($this->dom, $channel); + $this->_setDateCreated($this->dom, $channel); + $this->_setDateModified($this->dom, $channel); + $this->_setLastBuildDate($this->dom, $channel); + $this->_setGenerator($this->dom, $channel); + $this->_setLink($this->dom, $channel); + $this->_setAuthors($this->dom, $channel); + $this->_setCopyright($this->dom, $channel); + $this->_setCategories($this->dom, $channel); + + foreach ($this->extensions as $ext) { + $ext->setType($this->getType()); + $ext->setRootElement($this->getRootElement()); + $ext->setDOMDocument($this->getDOMDocument(), $channel); + $ext->render(); + } + + foreach ($this->container as $entry) { + if ($this->getDataContainer()->getEncoding()) { + $entry->setEncoding($this->getDataContainer()->getEncoding()); + } + if ($entry instanceof Writer\Entry) { + $renderer = new Renderer\Entry\Rss($entry); + } else { + continue; + } + if ($this->ignoreExceptions === true) { + $renderer->ignoreExceptions(); + } + $renderer->setType($this->getType()); + $renderer->setRootElement($this->dom->documentElement); + $renderer->render(); + $element = $renderer->getElement(); + $imported = $this->dom->importNode($element, true); + $channel->appendChild($imported); + } + return $this; + } + + /** + * Set feed language + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setLanguage(DOMDocument $dom, DOMElement $root) + { + $lang = $this->getDataContainer()->getLanguage(); + if (!$lang) { + return; + } + $language = $dom->createElement('language'); + $root->appendChild($language); + $language->nodeValue = $lang; + } + + /** + * Set feed title + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + * @throws Writer\Exception\InvalidArgumentException + */ + protected function _setTitle(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getTitle()) { + $message = 'RSS 2.0 feed elements MUST contain exactly one' + . ' title element but a title has not been set'; + $exception = new Writer\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + + $title = $dom->createElement('title'); + $root->appendChild($title); + $text = $dom->createTextNode($this->getDataContainer()->getTitle()); + $title->appendChild($text); + } + + /** + * Set feed description + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + * @throws Writer\Exception\InvalidArgumentException + */ + protected function _setDescription(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getDescription()) { + $message = 'RSS 2.0 feed elements MUST contain exactly one' + . ' description element but one has not been set'; + $exception = new Writer\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + $subtitle = $dom->createElement('description'); + $root->appendChild($subtitle); + $text = $dom->createTextNode($this->getDataContainer()->getDescription()); + $subtitle->appendChild($text); + } + + /** + * Set date feed was last modified + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setDateModified(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getDateModified()) { + return; + } + + $updated = $dom->createElement('pubDate'); + $root->appendChild($updated); + $text = $dom->createTextNode( + $this->getDataContainer()->getDateModified()->format(DateTime::RSS) + ); + $updated->appendChild($text); + } + + /** + * Set feed generator string + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setGenerator(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getGenerator()) { + $this->getDataContainer()->setGenerator('Zend_Feed_Writer', + Version::VERSION, 'http://framework.zend.com'); + } + + $gdata = $this->getDataContainer()->getGenerator(); + $generator = $dom->createElement('generator'); + $root->appendChild($generator); + $name = $gdata['name']; + if (array_key_exists('version', $gdata)) { + $name .= ' ' . $gdata['version']; + } + if (array_key_exists('uri', $gdata)) { + $name .= ' (' . $gdata['uri'] . ')'; + } + $text = $dom->createTextNode($name); + $generator->appendChild($text); + } + + /** + * Set link to feed + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + * @throws Writer\Exception\InvalidArgumentException + */ + protected function _setLink(DOMDocument $dom, DOMElement $root) + { + $value = $this->getDataContainer()->getLink(); + if (!$value) { + $message = 'RSS 2.0 feed elements MUST contain exactly one' + . ' link element but one has not been set'; + $exception = new Writer\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + $link = $dom->createElement('link'); + $root->appendChild($link); + $text = $dom->createTextNode($value); + $link->appendChild($text); + if (!Uri::factory($value)->isValid()) { + $link->setAttribute('isPermaLink', 'false'); + } + } + + /** + * Set feed authors + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setAuthors(DOMDocument $dom, DOMElement $root) + { + $authors = $this->getDataContainer()->getAuthors(); + if (!$authors || empty($authors)) { + return; + } + foreach ($authors as $data) { + $author = $this->dom->createElement('author'); + $name = $data['name']; + if (array_key_exists('email', $data)) { + $name = $data['email'] . ' (' . $data['name'] . ')'; + } + $text = $dom->createTextNode($name); + $author->appendChild($text); + $root->appendChild($author); + } + } + + /** + * Set feed copyright + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setCopyright(DOMDocument $dom, DOMElement $root) + { + $copyright = $this->getDataContainer()->getCopyright(); + if (!$copyright) { + return; + } + $copy = $dom->createElement('copyright'); + $root->appendChild($copy); + $text = $dom->createTextNode($copyright); + $copy->appendChild($text); + } + + /** + * Set feed channel image + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + * @throws Writer\Exception\InvalidArgumentException + */ + protected function _setImage(DOMDocument $dom, DOMElement $root) + { + $image = $this->getDataContainer()->getImage(); + if (!$image) { + return; + } + + if (!isset($image['title']) || empty($image['title']) + || !is_string($image['title']) + ) { + $message = 'RSS 2.0 feed images must include a title'; + $exception = new Writer\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + + if (empty($image['link']) || !is_string($image['link']) + || !Uri::factory($image['link'])->isValid() + ) { + $message = 'Invalid parameter: parameter \'link\'' + . ' must be a non-empty string and valid URI/IRI'; + $exception = new Writer\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + + $img = $dom->createElement('image'); + $root->appendChild($img); + + $url = $dom->createElement('url'); + $text = $dom->createTextNode($image['uri']); + $url->appendChild($text); + + $title = $dom->createElement('title'); + $text = $dom->createTextNode($image['title']); + $title->appendChild($text); + + $link = $dom->createElement('link'); + $text = $dom->createTextNode($image['link']); + $link->appendChild($text); + + $img->appendChild($url); + $img->appendChild($title); + $img->appendChild($link); + + if (isset($image['height'])) { + if (!ctype_digit((string) $image['height']) || $image['height'] > 400) { + $message = 'Invalid parameter: parameter \'height\'' + . ' must be an integer not exceeding 400'; + $exception = new Writer\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + $height = $dom->createElement('height'); + $text = $dom->createTextNode($image['height']); + $height->appendChild($text); + $img->appendChild($height); + } + if (isset($image['width'])) { + if (!ctype_digit((string) $image['width']) || $image['width'] > 144) { + $message = 'Invalid parameter: parameter \'width\'' + . ' must be an integer not exceeding 144'; + $exception = new Writer\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + $width = $dom->createElement('width'); + $text = $dom->createTextNode($image['width']); + $width->appendChild($text); + $img->appendChild($width); + } + if (isset($image['description'])) { + if (empty($image['description']) || !is_string($image['description'])) { + $message = 'Invalid parameter: parameter \'description\'' + . ' must be a non-empty string'; + $exception = new Writer\Exception\InvalidArgumentException($message); + if (!$this->ignoreExceptions) { + throw $exception; + } else { + $this->exceptions[] = $exception; + return; + } + } + $desc = $dom->createElement('description'); + $text = $dom->createTextNode($image['description']); + $desc->appendChild($text); + $img->appendChild($desc); + } + } + + /** + * Set date feed was created + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setDateCreated(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getDateCreated()) { + return; + } + if (!$this->getDataContainer()->getDateModified()) { + $this->getDataContainer()->setDateModified( + $this->getDataContainer()->getDateCreated() + ); + } + } + + /** + * Set date feed last build date + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setLastBuildDate(DOMDocument $dom, DOMElement $root) + { + if (!$this->getDataContainer()->getLastBuildDate()) { + return; + } + + $lastBuildDate = $dom->createElement('lastBuildDate'); + $root->appendChild($lastBuildDate); + $text = $dom->createTextNode( + $this->getDataContainer()->getLastBuildDate()->format(DateTime::RSS) + ); + $lastBuildDate->appendChild($text); + } + + /** + * Set base URL to feed links + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setBaseUrl(DOMDocument $dom, DOMElement $root) + { + $baseUrl = $this->getDataContainer()->getBaseUrl(); + if (!$baseUrl) { + return; + } + $root->setAttribute('xml:base', $baseUrl); + } + + /** + * Set feed categories + * + * @param DOMDocument $dom + * @param DOMElement $root + * @return void + */ + protected function _setCategories(DOMDocument $dom, DOMElement $root) + { + $categories = $this->getDataContainer()->getCategories(); + if (!$categories) { + return; + } + foreach ($categories as $cat) { + $category = $dom->createElement('category'); + if (isset($cat['scheme'])) { + $category->setAttribute('domain', $cat['scheme']); + } + $text = $dom->createTextNode($cat['term']); + $category->appendChild($text); + $root->appendChild($category); + } + } +} diff --git a/library/Zend/Feed/Writer/Renderer/RendererInterface.php b/library/Zend/Feed/Writer/Renderer/RendererInterface.php new file mode 100755 index 0000000000..b2e0e00a32 --- /dev/null +++ b/library/Zend/Feed/Writer/Renderer/RendererInterface.php @@ -0,0 +1,100 @@ + array(), + 'feed' => array(), + 'entryRenderer' => array(), + 'feedRenderer' => array(), + ); + + /** + * Set plugin loader for use with Extensions + * + * @param ExtensionManagerInterface + */ + public static function setExtensionManager(ExtensionManagerInterface $extensionManager) + { + static::$extensionManager = $extensionManager; + } + + /** + * Get plugin manager for use with Extensions + * + * @return ExtensionManagerInterface + */ + public static function getExtensionManager() + { + if (!isset(static::$extensionManager)) { + static::setExtensionManager(new ExtensionManager()); + } + return static::$extensionManager; + } + + /** + * Register an Extension by name + * + * @param string $name + * @return void + * @throws Exception\RuntimeException if unable to resolve Extension class + */ + public static function registerExtension($name) + { + $feedName = $name . '\Feed'; + $entryName = $name . '\Entry'; + $feedRendererName = $name . '\Renderer\Feed'; + $entryRendererName = $name . '\Renderer\Entry'; + $manager = static::getExtensionManager(); + if (static::isRegistered($name)) { + if ($manager->has($feedName) + || $manager->has($entryName) + || $manager->has($feedRendererName) + || $manager->has($entryRendererName) + ) { + return; + } + } + if (!$manager->has($feedName) + && !$manager->has($entryName) + && !$manager->has($feedRendererName) + && !$manager->has($entryRendererName) + ) { + throw new Exception\RuntimeException('Could not load extension: ' . $name + . 'using Plugin Loader. Check prefix paths are configured and extension exists.'); + } + if ($manager->has($feedName)) { + static::$extensions['feed'][] = $feedName; + } + if ($manager->has($entryName)) { + static::$extensions['entry'][] = $entryName; + } + if ($manager->has($feedRendererName)) { + static::$extensions['feedRenderer'][] = $feedRendererName; + } + if ($manager->has($entryRendererName)) { + static::$extensions['entryRenderer'][] = $entryRendererName; + } + } + + /** + * Is a given named Extension registered? + * + * @param string $extensionName + * @return bool + */ + public static function isRegistered($extensionName) + { + $feedName = $extensionName . '\Feed'; + $entryName = $extensionName . '\Entry'; + $feedRendererName = $extensionName . '\Renderer\Feed'; + $entryRendererName = $extensionName . '\Renderer\Entry'; + if (in_array($feedName, static::$extensions['feed']) + || in_array($entryName, static::$extensions['entry']) + || in_array($feedRendererName, static::$extensions['feedRenderer']) + || in_array($entryRendererName, static::$extensions['entryRenderer']) + ) { + return true; + } + return false; + } + + /** + * Get a list of extensions + * + * @return array + */ + public static function getExtensions() + { + return static::$extensions; + } + + /** + * Reset class state to defaults + * + * @return void + */ + public static function reset() + { + static::$extensionManager = null; + static::$extensions = array( + 'entry' => array(), + 'feed' => array(), + 'entryRenderer' => array(), + 'feedRenderer' => array(), + ); + } + + /** + * Register core (default) extensions + * + * @return void + */ + public static function registerCoreExtensions() + { + static::registerExtension('DublinCore'); + static::registerExtension('Content'); + static::registerExtension('Atom'); + static::registerExtension('Slash'); + static::registerExtension('WellFormedWeb'); + static::registerExtension('Threading'); + static::registerExtension('ITunes'); + } + + public static function lcfirst($str) + { + $str[0] = strtolower($str[0]); + return $str; + } +} diff --git a/library/Zend/Feed/composer.json b/library/Zend/Feed/composer.json new file mode 100755 index 0000000000..0c4d7dc181 --- /dev/null +++ b/library/Zend/Feed/composer.json @@ -0,0 +1,41 @@ +{ + "name": "zendframework/zend-feed", + "description": "provides functionality for consuming RSS and Atom feeds", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "feed" + ], + "homepage": "https://github.com/zendframework/zf2", + "autoload": { + "psr-0": { + "Zend\\Feed\\": "" + } + }, + "target-dir": "Zend/Feed", + "require": { + "php": ">=5.3.23", + "zendframework/zend-escaper": "self.version", + "zendframework/zend-stdlib": "self.version" + }, + "require-dev": { + "zendframework/zend-db": "self.version", + "zendframework/zend-cache": "self.version", + "zendframework/zend-http": "self.version", + "zendframework/zend-servicemanager": "self.version", + "zendframework/zend-validator": "self.version" + }, + "suggest": { + "zendframework/zend-cache": "Zend\\Cache component", + "zendframework/zend-db": "Zend\\Db component", + "zendframework/zend-http": "Zend\\Http for PubSubHubbub, and optionally for use with Zend\\Feed\\Reader", + "zendframework/zend-servicemanager": "Zend\\ServiceManager component, for default/recommended ExtensionManager implementations", + "zendframework/zend-validator": "Zend\\Validator component" + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev", + "dev-develop": "2.4-dev" + } + } +} diff --git a/library/Zend/File/CONTRIBUTING.md b/library/Zend/File/CONTRIBUTING.md new file mode 100755 index 0000000000..e77f5d2d5b --- /dev/null +++ b/library/Zend/File/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# CONTRIBUTING + +Please don't open pull requests against this repository, please use https://github.com/zendframework/zf2. \ No newline at end of file diff --git a/library/Zend/File/ClassFileLocator.php b/library/Zend/File/ClassFileLocator.php new file mode 100755 index 0000000000..9ef7821a60 --- /dev/null +++ b/library/Zend/File/ClassFileLocator.php @@ -0,0 +1,160 @@ +setInfoClass('Zend\File\PhpClassFile'); + } + + /** + * Filter for files containing PHP classes, interfaces, or abstracts + * + * @return bool + */ + public function accept() + { + $file = $this->getInnerIterator()->current(); + // If we somehow have something other than an SplFileInfo object, just + // return false + if (!$file instanceof SplFileInfo) { + return false; + } + + // If we have a directory, it's not a file, so return false + if (!$file->isFile()) { + return false; + } + + // If not a PHP file, skip + if ($file->getBasename('.php') == $file->getBasename()) { + return false; + } + + $contents = file_get_contents($file->getRealPath()); + $tokens = token_get_all($contents); + $count = count($tokens); + $t_trait = defined('T_TRAIT') ? T_TRAIT : -1; // For preserve PHP 5.3 compatibility + for ($i = 0; $i < $count; $i++) { + $token = $tokens[$i]; + if (!is_array($token)) { + // single character token found; skip + $i++; + continue; + } + switch ($token[0]) { + case T_NAMESPACE: + // Namespace found; grab it for later + $namespace = ''; + for ($i++; $i < $count; $i++) { + $token = $tokens[$i]; + if (is_string($token)) { + if (';' === $token) { + $saveNamespace = false; + break; + } + if ('{' === $token) { + $saveNamespace = true; + break; + } + continue; + } + list($type, $content, $line) = $token; + switch ($type) { + case T_STRING: + case T_NS_SEPARATOR: + $namespace .= $content; + break; + } + } + if ($saveNamespace) { + $savedNamespace = $namespace; + } + break; + case $t_trait: + case T_CLASS: + case T_INTERFACE: + // Abstract class, class, interface or trait found + + // Get the classname + for ($i++; $i < $count; $i++) { + $token = $tokens[$i]; + if (is_string($token)) { + continue; + } + list($type, $content, $line) = $token; + if (T_STRING == $type) { + // If a classname was found, set it in the object, and + // return boolean true (found) + if (!isset($namespace) || null === $namespace) { + if (isset($saveNamespace) && $saveNamespace) { + $namespace = $savedNamespace; + } else { + $namespace = null; + } + } + $class = (null === $namespace) ? $content : $namespace . '\\' . $content; + $file->addClass($class); + if ($namespace) { + $file->addNamespace($namespace); + } + $namespace = null; + break; + } + } + break; + default: + break; + } + } + $classes = $file->getClasses(); + if (!empty($classes)) { + return true; + } + // No class-type tokens found; return false + return false; + } +} diff --git a/library/Zend/File/Exception/BadMethodCallException.php b/library/Zend/File/Exception/BadMethodCallException.php new file mode 100755 index 0000000000..04be86936a --- /dev/null +++ b/library/Zend/File/Exception/BadMethodCallException.php @@ -0,0 +1,15 @@ +classes; + } + + /** + * Get namespaces + * + * @return array + */ + public function getNamespaces() + { + return $this->namespaces; + } + + /** + * Add class + * + * @param string $class + * @return self + */ + public function addClass($class) + { + $this->classes[] = $class; + return $this; + } + + /** + * Add namespace + * + * @param string $namespace + * @return self + */ + public function addNamespace($namespace) + { + if (in_array($namespace, $this->namespaces)) { + return $this; + } + $this->namespaces[] = $namespace; + return $this; + } +} diff --git a/library/Zend/File/README.md b/library/Zend/File/README.md new file mode 100755 index 0000000000..8e1ab787f5 --- /dev/null +++ b/library/Zend/File/README.md @@ -0,0 +1,15 @@ +File Component from ZF2 +======================= + +This is the File component for ZF2. + +- File issues at https://github.com/zendframework/zf2/issues +- Create pull requests against https://github.com/zendframework/zf2 +- Documentation is at http://framework.zend.com/docs + +LICENSE +------- + +The files in this archive are released under the [Zend Framework +license](http://framework.zend.com/license), which is a 3-clause BSD license. + diff --git a/library/Zend/File/Transfer/Adapter/AbstractAdapter.php b/library/Zend/File/Transfer/Adapter/AbstractAdapter.php new file mode 100755 index 0000000000..a46935f69e --- /dev/null +++ b/library/Zend/File/Transfer/Adapter/AbstractAdapter.php @@ -0,0 +1,1504 @@ + array( - Form is the name within the form or, if not set the filename + * name, - Original name of this file + * type, - Mime type of this file + * size, - Filesize in bytes + * tmp_name, - Internally temporary filename for uploaded files + * error, - Error which has occurred + * destination, - New destination for this file + * validators, - Set validator names for this file + * files - Set file names for this file + * )) + * + * @var array + */ + protected $files = array(); + + /** + * TMP directory + * @var string + */ + protected $tmpDir; + + /** + * Available options for file transfers + */ + protected $options = array( + 'ignoreNoFile' => false, + 'useByteString' => true, + 'magicFile' => null, + 'detectInfos' => true, + ); + + /** + * Send file + * + * @param mixed $options + * @return bool + */ + abstract public function send($options = null); + + /** + * Receive file + * + * @param mixed $options + * @return bool + */ + abstract public function receive($options = null); + + /** + * Is file sent? + * + * @param array|string|null $files + * @return bool + */ + abstract public function isSent($files = null); + + /** + * Is file received? + * + * @param array|string|null $files + * @return bool + */ + abstract public function isReceived($files = null); + + /** + * Has a file been uploaded ? + * + * @param array|string|null $files + * @return bool + */ + abstract public function isUploaded($files = null); + + /** + * Has the file been filtered ? + * + * @param array|string|null $files + * @return bool + */ + abstract public function isFiltered($files = null); + + /** + * Adds one or more files + * + * @param string|array $file File to add + * @param string|array $validator Validators to use for this file, must be set before + * @param string|array $filter Filters to use for this file, must be set before + * @return AbstractAdapter + * @throws Exception Not implemented + */ + //abstract public function addFile($file, $validator = null, $filter = null); + + /** + * Returns all set types + * + * @return array List of set types + * @throws Exception Not implemented + */ + //abstract public function getType(); + + /** + * Adds one or more type of files + * + * @param string|array $type Type of files to add + * @param string|array $validator Validators to use for this file, must be set before + * @param string|array $filter Filters to use for this file, must be set before + * @return AbstractAdapter + * @throws Exception Not implemented + */ + //abstract public function addType($type, $validator = null, $filter = null); + + /** + * Returns all set files + * + * @return array List of set files + */ + //abstract public function getFile(); + + /** + * Set the filter plugin manager instance + * + * @param FilterPluginManager $filterManager + * @return AbstractAdapter + */ + public function setFilterManager(FilterPluginManager $filterManager) + { + $this->filterManager = $filterManager; + return $this; + } + + /** + * Get the filter plugin manager instance + * + * @return FilterPluginManager + */ + public function getFilterManager() + { + if (!$this->filterManager instanceof FilterPluginManager) { + $this->setFilterManager(new FilterPluginManager()); + } + return $this->filterManager; + } + + /** + * Set the validator plugin manager instance + * + * @param ValidatorPluginManager $validatorManager + * @return AbstractAdapter + */ + public function setValidatorManager(ValidatorPluginManager $validatorManager) + { + $this->validatorManager = $validatorManager; + return $this; + } + + /** + * Get the validator plugin manager instance + * + * @return ValidatorPluginManager + */ + public function getValidatorManager() + { + if (!$this->validatorManager instanceof ValidatorPluginManager) { + $this->setValidatorManager(new ValidatorPluginManager()); + } + return $this->validatorManager; + } + + /** + * Adds a new validator for this class + * + * @param string|Validator\ValidatorInterface $validator Type of validator to add + * @param bool $breakChainOnFailure If the validation chain should stop a failure + * @param string|array $options Options to set for the validator + * @param string|array $files Files to limit this validator to + * @return AbstractAdapter + * @throws Exception\InvalidArgumentException for invalid type + */ + public function addValidator($validator, $breakChainOnFailure = false, $options = null, $files = null) + { + if (is_string($validator)) { + $validator = $this->getValidatorManager()->get($validator, $options); + if (is_array($options) && isset($options['messages'])) { + if (is_array($options['messages'])) { + $validator->setMessages($options['messages']); + } elseif (is_string($options['messages'])) { + $validator->setMessage($options['messages']); + } + + unset($options['messages']); + } + } + + if (!$validator instanceof Validator\ValidatorInterface) { + throw new Exception\InvalidArgumentException( + 'Invalid validator provided to addValidator; ' . + 'must be string or Zend\Validator\ValidatorInterface' + ); + } + + $name = get_class($validator); + + $this->validators[$name] = $validator; + $this->break[$name] = $breakChainOnFailure; + $files = $this->getFiles($files, true, true); + foreach ($files as $file) { + if ($name == 'NotEmpty') { + $temp = $this->files[$file]['validators']; + $this->files[$file]['validators'] = array($name); + $this->files[$file]['validators'] += $temp; + } else { + $this->files[$file]['validators'][] = $name; + } + + $this->files[$file]['validated'] = false; + } + + return $this; + } + + /** + * Add Multiple validators at once + * + * @param array $validators + * @param string|array $files + * @return AbstractAdapter + * @throws Exception\InvalidArgumentException for invalid type + */ + public function addValidators(array $validators, $files = null) + { + foreach ($validators as $name => $validatorInfo) { + if ($validatorInfo instanceof Validator\ValidatorInterface) { + $this->addValidator($validatorInfo, null, null, $files); + } elseif (is_string($validatorInfo)) { + if (!is_int($name)) { + $this->addValidator($name, null, $validatorInfo, $files); + } else { + $this->addValidator($validatorInfo, null, null, $files); + } + } elseif (is_array($validatorInfo)) { + $argc = count($validatorInfo); + $breakChainOnFailure = false; + $options = array(); + if (isset($validatorInfo['validator'])) { + $validator = $validatorInfo['validator']; + if (isset($validatorInfo['breakChainOnFailure'])) { + $breakChainOnFailure = $validatorInfo['breakChainOnFailure']; + } + + if (isset($validatorInfo['options'])) { + $options = $validatorInfo['options']; + } + + $this->addValidator($validator, $breakChainOnFailure, $options, $files); + } else { + if (is_string($name)) { + $validator = $name; + $options = $validatorInfo; + $this->addValidator($validator, $breakChainOnFailure, $options, $files); + } else { + $file = $files; + switch (true) { + case (0 == $argc): + break; + case (1 <= $argc): + $validator = array_shift($validatorInfo); + case (2 <= $argc): + $breakChainOnFailure = array_shift($validatorInfo); + case (3 <= $argc): + $options = array_shift($validatorInfo); + case (4 <= $argc): + if (!empty($validatorInfo)) { + $file = array_shift($validatorInfo); + } + default: + $this->addValidator($validator, $breakChainOnFailure, $options, $file); + break; + } + } + } + } else { + throw new Exception\InvalidArgumentException('Invalid validator passed to addValidators()'); + } + } + + return $this; + } + + /** + * Sets a validator for the class, erasing all previous set + * + * @param array $validators Validators to set + * @param string|array $files Files to limit this validator to + * @return AbstractAdapter + */ + public function setValidators(array $validators, $files = null) + { + $this->clearValidators(); + return $this->addValidators($validators, $files); + } + + /** + * Determine if a given validator has already been registered + * + * @param string $name + * @return bool + */ + public function hasValidator($name) + { + return (false !== $this->getValidatorIdentifier($name)); + } + + /** + * Retrieve individual validator + * + * @param string $name + * @return Validator\ValidatorInterface|null + */ + public function getValidator($name) + { + if (false === ($identifier = $this->getValidatorIdentifier($name))) { + return null; + } + return $this->validators[$identifier]; + } + + /** + * Returns all set validators + * + * @param string|array $files (Optional) Returns the validator for this files + * @return null|array List of set validators + */ + public function getValidators($files = null) + { + if ($files == null) { + return $this->validators; + } + + $files = $this->getFiles($files, true, true); + $validators = array(); + foreach ($files as $file) { + if (!empty($this->files[$file]['validators'])) { + $validators += $this->files[$file]['validators']; + } + } + + $validators = array_unique($validators); + $result = array(); + foreach ($validators as $validator) { + $result[$validator] = $this->validators[$validator]; + } + + return $result; + } + + /** + * Remove an individual validator + * + * @param string $name + * @return AbstractAdapter + */ + public function removeValidator($name) + { + if (false === ($key = $this->getValidatorIdentifier($name))) { + return $this; + } + + unset($this->validators[$key]); + foreach (array_keys($this->files) as $file) { + if (empty($this->files[$file]['validators'])) { + continue; + } + + $index = array_search($key, $this->files[$file]['validators']); + if ($index === false) { + continue; + } + + unset($this->files[$file]['validators'][$index]); + $this->files[$file]['validated'] = false; + } + + return $this; + } + + /** + * Remove all validators + * + * @return AbstractAdapter + */ + public function clearValidators() + { + $this->validators = array(); + foreach (array_keys($this->files) as $file) { + $this->files[$file]['validators'] = array(); + $this->files[$file]['validated'] = false; + } + + return $this; + } + + /** + * Sets Options for adapters + * + * @param array $options Options to set + * @param array $files (Optional) Files to set the options for + * @return AbstractAdapter + */ + public function setOptions($options = array(), $files = null) + { + $file = $this->getFiles($files, false, true); + + if (is_array($options)) { + if (empty($file)) { + $this->options = array_merge($this->options, $options); + } + + foreach ($options as $name => $value) { + foreach ($file as $key => $content) { + switch ($name) { + case 'magicFile' : + $this->files[$key]['options'][$name] = (string) $value; + break; + + case 'ignoreNoFile' : + case 'useByteString' : + case 'detectInfos' : + $this->files[$key]['options'][$name] = (bool) $value; + break; + + default: + continue; + } + } + } + } + + return $this; + } + + /** + * Returns set options for adapters or files + * + * @param array $files (Optional) Files to return the options for + * @return array Options for given files + */ + public function getOptions($files = null) + { + $file = $this->getFiles($files, false, true); + + foreach ($file as $key => $content) { + if (isset($this->files[$key]['options'])) { + $options[$key] = $this->files[$key]['options']; + } else { + $options[$key] = array(); + } + } + + return $options; + } + + /** + * Checks if the files are valid + * + * @param string|array $files (Optional) Files to check + * @return bool True if all checks are valid + */ + public function isValid($files = null) + { + $check = $this->getFiles($files, false, true); + if (empty($check)) { + return false; + } + + $translator = $this->getTranslator(); + $this->messages = array(); + $break = false; + foreach ($check as $content) { + if (array_key_exists('validators', $content) && + in_array('Zend\Validator\File\Count', $content['validators'])) { + $validator = $this->validators['Zend\Validator\File\Count']; + $count = $content; + if (empty($content['tmp_name'])) { + continue; + } + + if (array_key_exists('destination', $content)) { + $checkit = $content['destination']; + } else { + $checkit = dirname($content['tmp_name']); + } + + $checkit .= DIRECTORY_SEPARATOR . $content['name']; + $validator->addFile($checkit); + } + } + + if (isset($count)) { + if (!$validator->isValid($count['tmp_name'], $count)) { + $this->messages += $validator->getMessages(); + } + } + + foreach ($check as $key => $content) { + $fileerrors = array(); + if (array_key_exists('validators', $content) && $content['validated']) { + continue; + } + + if (array_key_exists('validators', $content)) { + foreach ($content['validators'] as $class) { + $validator = $this->validators[$class]; + if (method_exists($validator, 'setTranslator')) { + $validator->setTranslator($translator); + } + + if (($class === 'Zend\Validator\File\Upload') && (empty($content['tmp_name']))) { + $tocheck = $key; + } else { + $tocheck = $content['tmp_name']; + } + + if (!$validator->isValid($tocheck, $content)) { + $fileerrors += $validator->getMessages(); + } + + if (!empty($content['options']['ignoreNoFile']) && (isset($fileerrors['fileUploadErrorNoFile']))) { + unset($fileerrors['fileUploadErrorNoFile']); + break; + } + + if (($class === 'Zend\Validator\File\Upload') && (count($fileerrors) > 0)) { + break; + } + + if (($this->break[$class]) && (count($fileerrors) > 0)) { + $break = true; + break; + } + } + } + + if (count($fileerrors) > 0) { + $this->files[$key]['validated'] = false; + } else { + $this->files[$key]['validated'] = true; + } + + $this->messages += $fileerrors; + if ($break) { + break; + } + } + + if (count($this->messages) > 0) { + return false; + } + + return true; + } + + /** + * Returns found validation messages + * + * @return array + */ + public function getMessages() + { + return $this->messages; + } + + /** + * Retrieve error codes + * + * @return array + */ + public function getErrors() + { + return array_keys($this->messages); + } + + /** + * Are there errors registered? + * + * @return bool + */ + public function hasErrors() + { + return (!empty($this->messages)); + } + + /** + * Adds a new filter for this class + * + * @param string|Filter\FilterInterface $filter Type of filter to add + * @param string|array $options Options to set for the filter + * @param string|array $files Files to limit this filter to + * @return AbstractAdapter + * @throws Exception\InvalidArgumentException for invalid type + */ + public function addFilter($filter, $options = null, $files = null) + { + if (is_string($filter)) { + $filter = $this->getFilterManager()->get($filter, $options); + } + + if (!$filter instanceof Filter\FilterInterface) { + throw new Exception\InvalidArgumentException('Invalid filter specified'); + } + + $class = get_class($filter); + $this->filters[$class] = $filter; + $files = $this->getFiles($files, true, true); + foreach ($files as $file) { + $this->files[$file]['filters'][] = $class; + } + + return $this; + } + + /** + * Add Multiple filters at once + * + * @param array $filters + * @param string|array $files + * @return AbstractAdapter + */ + public function addFilters(array $filters, $files = null) + { + foreach ($filters as $key => $spec) { + if ($spec instanceof Filter\FilterInterface) { + $this->addFilter($spec, null, $files); + continue; + } + + if (is_string($key)) { + $this->addFilter($key, $spec, $files); + continue; + } + + if (is_int($key)) { + if (is_string($spec)) { + $this->addFilter($spec, null, $files); + continue; + } + + if (is_array($spec)) { + if (!array_key_exists('filter', $spec)) { + continue; + } + + $filter = $spec['filter']; + unset($spec['filter']); + $this->addFilter($filter, $spec, $files); + continue; + } + + continue; + } + } + + return $this; + } + + /** + * Sets a filter for the class, erasing all previous set + * + * @param array $filters Filter to set + * @param string|array $files Files to limit this filter to + * @return Filter\AbstractFilter + */ + public function setFilters(array $filters, $files = null) + { + $this->clearFilters(); + return $this->addFilters($filters, $files); + } + + /** + * Determine if a given filter has already been registered + * + * @param string $name + * @return bool + */ + public function hasFilter($name) + { + return (false !== $this->getFilterIdentifier($name)); + } + + /** + * Retrieve individual filter + * + * @param string $name + * @return Filter\FilterInterface|null + */ + public function getFilter($name) + { + if (false === ($identifier = $this->getFilterIdentifier($name))) { + return null; + } + + return $this->filters[$identifier]; + } + + /** + * Returns all set filters + * + * @param string|array $files (Optional) Returns the filter for this files + * @return array List of set filters + * @throws Exception\RuntimeException When file not found + */ + public function getFilters($files = null) + { + if ($files === null) { + return $this->filters; + } + + $files = $this->getFiles($files, true, true); + $filters = array(); + foreach ($files as $file) { + if (!empty($this->files[$file]['filters'])) { + $filters += $this->files[$file]['filters']; + } + } + + $filters = array_unique($filters); + $result = array(); + foreach ($filters as $filter) { + $result[] = $this->filters[$filter]; + } + + return $result; + } + + /** + * Remove an individual filter + * + * @param string $name + * @return AbstractAdapter + */ + public function removeFilter($name) + { + if (false === ($key = $this->getFilterIdentifier($name))) { + return $this; + } + + unset($this->filters[$key]); + foreach (array_keys($this->files) as $file) { + if (empty($this->files[$file]['filters'])) { + continue; + } + + $index = array_search($key, $this->files[$file]['filters']); + if ($index === false) { + continue; + } + + unset($this->files[$file]['filters'][$index]); + } + return $this; + } + + /** + * Remove all filters + * + * @return AbstractAdapter + */ + public function clearFilters() + { + $this->filters = array(); + foreach (array_keys($this->files) as $file) { + $this->files[$file]['filters'] = array(); + } + + return $this; + } + + /** + * Retrieves the filename of transferred files. + * + * @param string $file (Optional) Element to return the filename for + * @param bool $path (Optional) Should the path also be returned ? + * @return string|array + */ + public function getFileName($file = null, $path = true) + { + $files = $this->getFiles($file, true, true); + $result = array(); + $directory = ""; + foreach ($files as $file) { + if (empty($this->files[$file]['name'])) { + continue; + } + + if ($path === true) { + $directory = $this->getDestination($file) . DIRECTORY_SEPARATOR; + } + + $result[$file] = $directory . $this->files[$file]['name']; + } + + if (count($result) == 1) { + return current($result); + } + + return $result; + } + + /** + * Retrieve additional internal file informations for files + * + * @param string $file (Optional) File to get informations for + * @return array + */ + public function getFileInfo($file = null) + { + return $this->getFiles($file); + } + + /** + * Sets a new destination for the given files + * + * @deprecated Will be changed to be a filter!!! + * @param string $destination New destination directory + * @param string|array $files Files to set the new destination for + * @return AbstractAdapter + * @throws Exception\InvalidArgumentException when the given destination is not a directory or does not exist + */ + public function setDestination($destination, $files = null) + { + $orig = $files; + $destination = rtrim($destination, "/\\"); + if (!is_dir($destination)) { + throw new Exception\InvalidArgumentException('The given destination is not a directory or does not exist'); + } + + if (!is_writable($destination)) { + throw new Exception\InvalidArgumentException('The given destination is not writeable'); + } + + if ($files === null) { + foreach ($this->files as $file => $content) { + $this->files[$file]['destination'] = $destination; + } + } else { + $files = $this->getFiles($files, true, true); + if (empty($files) and is_string($orig)) { + $this->files[$orig]['destination'] = $destination; + } + + foreach ($files as $file) { + $this->files[$file]['destination'] = $destination; + } + } + + return $this; + } + + /** + * Retrieve destination directory value + * + * @param null|string|array $files + * @throws Exception\InvalidArgumentException + * @return null|string|array + */ + public function getDestination($files = null) + { + $orig = $files; + $files = $this->getFiles($files, false, true); + $destinations = array(); + if (empty($files) and is_string($orig)) { + if (isset($this->files[$orig]['destination'])) { + $destinations[$orig] = $this->files[$orig]['destination']; + } else { + throw new Exception\InvalidArgumentException( + sprintf('The file transfer adapter can not find "%s"', $orig) + ); + } + } + + foreach ($files as $key => $content) { + if (isset($this->files[$key]['destination'])) { + $destinations[$key] = $this->files[$key]['destination']; + } else { + $tmpdir = $this->getTmpDir(); + $this->setDestination($tmpdir, $key); + $destinations[$key] = $tmpdir; + } + } + + if (empty($destinations)) { + $destinations = $this->getTmpDir(); + } elseif (count($destinations) == 1) { + $destinations = current($destinations); + } + + return $destinations; + } + + /** + * Sets translator to use in helper + * + * @param Translator $translator [optional] translator. + * Default is null, which sets no translator. + * @param string $textDomain [optional] text domain + * Default is null, which skips setTranslatorTextDomain + * @return AbstractAdapter + */ + public function setTranslator(Translator $translator = null, $textDomain = null) + { + $this->translator = $translator; + if (null !== $textDomain) { + $this->setTranslatorTextDomain($textDomain); + } + return $this; + } + + /** + * Retrieve localization translator object + * + * @return Translator|null + */ + public function getTranslator() + { + if ($this->isTranslatorEnabled()) { + return null; + } + + return $this->translator; + } + + /** + * Checks if the helper has a translator + * + * @return bool + */ + public function hasTranslator() + { + return (bool) $this->getTranslator(); + } + + /** + * Indicate whether or not translation should be enabled + * + * @param bool $flag + * @return AbstractAdapter + */ + public function setTranslatorEnabled($flag = true) + { + $this->translatorEnabled = (bool) $flag; + return $this; + } + + /** + * Is translation enabled? + * + * @return bool + */ + public function isTranslatorEnabled() + { + return $this->translatorEnabled; + } + + /** + * Set translation text domain + * + * @param string $textDomain + * @return AbstractAdapter + */ + public function setTranslatorTextDomain($textDomain = 'default') + { + $this->translatorTextDomain = $textDomain; + return $this; + } + + /** + * Return the translation text domain + * + * @return string + */ + public function getTranslatorTextDomain() + { + return $this->translatorTextDomain; + } + + /** + * Returns the hash for a given file + * + * @param string $hash Hash algorithm to use + * @param string|array $files Files to return the hash for + * @return string|array Hashstring + * @throws Exception\InvalidArgumentException On unknown hash algorithm + */ + public function getHash($hash = 'crc32', $files = null) + { + if (!in_array($hash, hash_algos())) { + throw new Exception\InvalidArgumentException('Unknown hash algorithm'); + } + + $files = $this->getFiles($files); + $result = array(); + foreach ($files as $key => $value) { + if (file_exists($value['name'])) { + $result[$key] = hash_file($hash, $value['name']); + } elseif (file_exists($value['tmp_name'])) { + $result[$key] = hash_file($hash, $value['tmp_name']); + } elseif (empty($value['options']['ignoreNoFile'])) { + throw new Exception\InvalidArgumentException("The file '{$value['name']}' does not exist"); + } + } + + if (count($result) == 1) { + return current($result); + } + + return $result; + } + + /** + * Returns the real filesize of the file + * + * @param string|array $files Files to get the filesize from + * @return string|array Filesize + * @throws Exception\InvalidArgumentException When the file does not exist + */ + public function getFileSize($files = null) + { + $files = $this->getFiles($files); + $result = array(); + foreach ($files as $key => $value) { + if (file_exists($value['name']) || file_exists($value['tmp_name'])) { + if ($value['options']['useByteString']) { + $result[$key] = static::toByteString($value['size']); + } else { + $result[$key] = $value['size']; + } + } elseif (empty($value['options']['ignoreNoFile'])) { + throw new Exception\InvalidArgumentException("The file '{$value['name']}' does not exist"); + } else { + continue; + } + } + + if (count($result) == 1) { + return current($result); + } + + return $result; + } + + /** + * Internal method to detect the size of a file + * + * @param array $value File infos + * @return string Filesize of given file + */ + protected function detectFileSize($value) + { + if (file_exists($value['name'])) { + $filename = $value['name']; + } elseif (file_exists($value['tmp_name'])) { + $filename = $value['tmp_name']; + } else { + return null; + } + + ErrorHandler::start(); + $filesize = filesize($filename); + $return = ErrorHandler::stop(); + if ($return instanceof ErrorException) { + $filesize = 0; + } + + return sprintf("%u", $filesize); + } + + /** + * Returns the real mimetype of the file + * Uses fileinfo, when not available mime_magic and as last fallback a manual given mimetype + * + * @param string|array $files Files to get the mimetype from + * @return string|array MimeType + * @throws Exception\InvalidArgumentException When the file does not exist + */ + public function getMimeType($files = null) + { + $files = $this->getFiles($files); + $result = array(); + foreach ($files as $key => $value) { + if (file_exists($value['name']) || file_exists($value['tmp_name'])) { + $result[$key] = $value['type']; + } elseif (empty($value['options']['ignoreNoFile'])) { + throw new Exception\InvalidArgumentException("the file '{$value['name']}' does not exist"); + } else { + continue; + } + } + + if (count($result) == 1) { + return current($result); + } + + return $result; + } + + /** + * Internal method to detect the mime type of a file + * + * @param array $value File infos + * @return string Mimetype of given file + */ + protected function detectMimeType($value) + { + if (file_exists($value['name'])) { + $file = $value['name']; + } elseif (file_exists($value['tmp_name'])) { + $file = $value['tmp_name']; + } else { + return null; + } + + if (class_exists('finfo', false)) { + if (!empty($value['options']['magicFile'])) { + ErrorHandler::start(); + $mime = finfo_open(FILEINFO_MIME_TYPE, $value['options']['magicFile']); + ErrorHandler::stop(); + } + + if (empty($mime)) { + ErrorHandler::start(); + $mime = finfo_open(FILEINFO_MIME_TYPE); + ErrorHandler::stop(); + } + + if (!empty($mime)) { + $result = finfo_file($mime, $file); + } + + unset($mime); + } + + if (empty($result) && (function_exists('mime_content_type') + && ini_get('mime_magic.magicfile'))) { + $result = mime_content_type($file); + } + + if (empty($result)) { + $result = 'application/octet-stream'; + } + + return $result; + } + + /** + * Returns the formatted size + * + * @param int $size + * @return string + */ + protected static function toByteString($size) + { + $sizes = array('B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'); + for ($i=0; $size >= 1024 && $i < 9; $i++) { + $size /= 1024; + } + + return round($size, 2) . $sizes[$i]; + } + + /** + * Internal function to filter all given files + * + * @param string|array $files (Optional) Files to check + * @return bool False on error + */ + protected function filter($files = null) + { + $check = $this->getFiles($files); + foreach ($check as $name => $content) { + if (array_key_exists('filters', $content)) { + foreach ($content['filters'] as $class) { + $filter = $this->filters[$class]; + try { + $result = $filter->filter($this->getFileName($name)); + + $this->files[$name]['destination'] = dirname($result); + $this->files[$name]['name'] = basename($result); + } catch (FilterException\ExceptionInterface $e) { + $this->messages += array($e->getMessage()); + } + } + } + } + + if (count($this->messages) > 0) { + return false; + } + + return true; + } + + /** + * Determine system TMP directory and detect if we have read access + * + * @return string + * @throws Exception\RuntimeException if unable to determine directory + */ + protected function getTmpDir() + { + if (null === $this->tmpDir) { + $tmpdir = array(); + if (function_exists('sys_get_temp_dir')) { + $tmpdir[] = sys_get_temp_dir(); + } + + if (!empty($_ENV['TMP'])) { + $tmpdir[] = realpath($_ENV['TMP']); + } + + if (!empty($_ENV['TMPDIR'])) { + $tmpdir[] = realpath($_ENV['TMPDIR']); + } + + if (!empty($_ENV['TEMP'])) { + $tmpdir[] = realpath($_ENV['TEMP']); + } + + $upload = ini_get('upload_tmp_dir'); + if ($upload) { + $tmpdir[] = realpath($upload); + } + + foreach ($tmpdir as $directory) { + if ($this->isPathWriteable($directory)) { + $this->tmpDir = $directory; + } + } + + if (empty($this->tmpDir)) { + // Attemp to detect by creating a temporary file + $tempFile = tempnam(md5(uniqid(rand(), true)), ''); + if ($tempFile) { + $this->tmpDir = realpath(dirname($tempFile)); + unlink($tempFile); + } else { + throw new Exception\RuntimeException('Could not determine a temporary directory'); + } + } + + $this->tmpDir = rtrim($this->tmpDir, "/\\"); + } + return $this->tmpDir; + } + + /** + * Tries to detect if we can read and write to the given path + * + * @param string $path + * @return bool + */ + protected function isPathWriteable($path) + { + $tempFile = rtrim($path, "/\\"); + $tempFile .= '/' . 'test.1'; + + ErrorHandler::start(); + $result = file_put_contents($tempFile, 'TEST'); + ErrorHandler::stop(); + + if ($result == false) { + return false; + } + + ErrorHandler::start(); + $result = unlink($tempFile); + ErrorHandler::stop(); + + if ($result == false) { + return false; + } + + return true; + } + + /** + * Returns found files based on internal file array and given files + * + * @param string|array $files (Optional) Files to return + * @param bool $names (Optional) Returns only names on true, else complete info + * @param bool $noexception (Optional) Allows throwing an exception, otherwise returns an empty array + * @return array Found files + * @throws Exception\RuntimeException On false filename + */ + protected function getFiles($files, $names = false, $noexception = false) + { + $check = array(); + + if (is_string($files)) { + $files = array($files); + } + + if (is_array($files)) { + foreach ($files as $find) { + $found = array(); + foreach ($this->files as $file => $content) { + if (!isset($content['name'])) { + continue; + } + + if (($content['name'] === $find) && isset($content['multifiles'])) { + foreach ($content['multifiles'] as $multifile) { + $found[] = $multifile; + } + break; + } + + if ($file === $find) { + $found[] = $file; + break; + } + + if ($content['name'] === $find) { + $found[] = $file; + break; + } + } + + if (empty($found)) { + if ($noexception !== false) { + return array(); + } + + throw new Exception\RuntimeException(sprintf('The file transfer adapter can not find "%s"', $find)); + } + + foreach ($found as $checked) { + $check[$checked] = $this->files[$checked]; + } + } + } + + if ($files === null) { + $check = $this->files; + $keys = array_keys($check); + foreach ($keys as $key) { + if (isset($check[$key]['multifiles'])) { + unset($check[$key]); + } + } + } + + if ($names) { + $check = array_keys($check); + } + + return $check; + } + + /** + * Retrieve internal identifier for a named validator + * + * @param string $name + * @return string + */ + protected function getValidatorIdentifier($name) + { + if (array_key_exists($name, $this->validators)) { + return $name; + } + + foreach (array_keys($this->validators) as $test) { + if (preg_match('/' . preg_quote($name) . '$/i', $test)) { + return $test; + } + } + + return false; + } + + /** + * Retrieve internal identifier for a named filter + * + * @param string $name + * @return string + */ + protected function getFilterIdentifier($name) + { + if (array_key_exists($name, $this->filters)) { + return $name; + } + + foreach (array_keys($this->filters) as $test) { + if (preg_match('/' . preg_quote($name) . '$/i', $test)) { + return $test; + } + } + + return false; + } +} diff --git a/library/Zend/File/Transfer/Adapter/FilterPluginManager.php b/library/Zend/File/Transfer/Adapter/FilterPluginManager.php new file mode 100755 index 0000000000..28da7ee4a0 --- /dev/null +++ b/library/Zend/File/Transfer/Adapter/FilterPluginManager.php @@ -0,0 +1,35 @@ + 'filedecrypt', + 'encrypt' => 'fileencrypt', + 'lowercase' => 'filelowercase', + 'rename' => 'filerename', + 'uppercase' => 'fileuppercase', + ); +} diff --git a/library/Zend/File/Transfer/Adapter/Http.php b/library/Zend/File/Transfer/Adapter/Http.php new file mode 100755 index 0000000000..3cba2184d5 --- /dev/null +++ b/library/Zend/File/Transfer/Adapter/Http.php @@ -0,0 +1,464 @@ +setOptions($options); + $this->prepareFiles(); + $this->addValidator('Upload', false, $this->files); + } + + /** + * Sets a validator for the class, erasing all previous set + * + * @param array $validators Validator to set + * @param string|array $files Files to limit this validator to + * @return AbstractAdapter + */ + public function setValidators(array $validators, $files = null) + { + $this->clearValidators(); + return $this->addValidators($validators, $files); + } + + /** + * Remove an individual validator + * + * @param string $name + * @return AbstractAdapter + */ + public function removeValidator($name) + { + if ($name == 'Upload') { + return $this; + } + + return parent::removeValidator($name); + } + + /** + * Clear the validators + * + * @return AbstractAdapter + */ + public function clearValidators() + { + parent::clearValidators(); + $this->addValidator('Upload', false, $this->files); + + return $this; + } + + /** + * Send the file to the client (Download) + * + * @param string|array $options Options for the file(s) to send + * @return void + * @throws Exception\BadMethodCallException Not implemented + */ + public function send($options = null) + { + throw new Exception\BadMethodCallException('Method not implemented'); + } + + /** + * Checks if the files are valid + * + * @param string|array $files (Optional) Files to check + * @return bool True if all checks are valid + */ + public function isValid($files = null) + { + // Workaround for WebServer not conforming HTTP and omitting CONTENT_LENGTH + $content = 0; + if (isset($_SERVER['CONTENT_LENGTH'])) { + $content = $_SERVER['CONTENT_LENGTH']; + } elseif (!empty($_POST)) { + $content = serialize($_POST); + } + + // Workaround for a PHP error returning empty $_FILES when form data exceeds php settings + if (empty($this->files) && ($content > 0)) { + if (is_array($files)) { + $files = current($files); + } + + $temp = array($files => array( + 'name' => $files, + 'error' => 1)); + $validator = $this->validators['Zend\Validator\File\Upload']; + $validator->setTranslator($this->getTranslator()) + ->setFiles($temp) + ->isValid($files, null); + $this->messages += $validator->getMessages(); + return false; + } + + return parent::isValid($files); + } + + /** + * Receive the file from the client (Upload) + * + * @param string|array $files (Optional) Files to receive + * @return bool + */ + public function receive($files = null) + { + if (!$this->isValid($files)) { + return false; + } + + $check = $this->getFiles($files); + foreach ($check as $file => $content) { + if (!$content['received']) { + $directory = ''; + $destination = $this->getDestination($file); + if ($destination !== null) { + $directory = $destination . DIRECTORY_SEPARATOR; + } + + $filename = $directory . $content['name']; + $rename = $this->getFilter('Rename'); + if ($rename !== null) { + $tmp = $rename->getNewName($content['tmp_name']); + if ($tmp != $content['tmp_name']) { + $filename = $tmp; + } + + if (dirname($filename) == '.') { + $filename = $directory . $filename; + } + + $key = array_search(get_class($rename), $this->files[$file]['filters']); + unset($this->files[$file]['filters'][$key]); + } + + // Should never return false when it's tested by the upload validator + if (!move_uploaded_file($content['tmp_name'], $filename)) { + if ($content['options']['ignoreNoFile']) { + $this->files[$file]['received'] = true; + $this->files[$file]['filtered'] = true; + continue; + } + + $this->files[$file]['received'] = false; + return false; + } + + if ($rename !== null) { + $this->files[$file]['destination'] = dirname($filename); + $this->files[$file]['name'] = basename($filename); + } + + $this->files[$file]['tmp_name'] = $filename; + $this->files[$file]['received'] = true; + } + + if (!$content['filtered']) { + if (!$this->filter($file)) { + $this->files[$file]['filtered'] = false; + return false; + } + + $this->files[$file]['filtered'] = true; + } + } + + return true; + } + + /** + * Checks if the file was already sent + * + * @param string|array $files Files to check + * @return bool + * @throws Exception\BadMethodCallException Not implemented + */ + public function isSent($files = null) + { + throw new Exception\BadMethodCallException('Method not implemented'); + } + + /** + * Checks if the file was already received + * + * @param string|array $files (Optional) Files to check + * @return bool + */ + public function isReceived($files = null) + { + $files = $this->getFiles($files, false, true); + if (empty($files)) { + return false; + } + + foreach ($files as $content) { + if ($content['received'] !== true) { + return false; + } + } + + return true; + } + + /** + * Checks if the file was already filtered + * + * @param string|array $files (Optional) Files to check + * @return bool + */ + public function isFiltered($files = null) + { + $files = $this->getFiles($files, false, true); + if (empty($files)) { + return false; + } + + foreach ($files as $content) { + if ($content['filtered'] !== true) { + return false; + } + } + + return true; + } + + /** + * Has a file been uploaded ? + * + * @param array|string|null $files + * @return bool + */ + public function isUploaded($files = null) + { + $files = $this->getFiles($files, false, true); + if (empty($files)) { + return false; + } + + foreach ($files as $file) { + if (empty($file['name'])) { + return false; + } + } + + return true; + } + + /** + * Returns the actual progress of file up-/downloads + * + * @param string|array $id The upload to get the progress for + * @return array|null + * @throws Exception\PhpEnvironmentException whether APC nor UploadProgress extension installed + * @throws Exception\RuntimeException + */ + public static function getProgress($id = null) + { + if (!static::isApcAvailable() && !static::isUploadProgressAvailable()) { + throw new Exception\PhpEnvironmentException('Neither APC nor UploadProgress extension installed'); + } + + $session = 'Zend\File\Transfer\Adapter\Http\ProgressBar'; + $status = array( + 'total' => 0, + 'current' => 0, + 'rate' => 0, + 'message' => '', + 'done' => false + ); + + if (is_array($id)) { + if (isset($id['progress'])) { + $adapter = $id['progress']; + } + + if (isset($id['session'])) { + $session = $id['session']; + } + + if (isset($id['id'])) { + $id = $id['id']; + } else { + unset($id); + } + } + + if (!empty($id) && (($id instanceof Adapter\AbstractAdapter) || ($id instanceof ProgressBar\ProgressBar))) { + $adapter = $id; + unset($id); + } + + if (empty($id)) { + if (!isset($_GET['progress_key'])) { + $status['message'] = 'No upload in progress'; + $status['done'] = true; + } else { + $id = $_GET['progress_key']; + } + } + + if (!empty($id)) { + if (static::isApcAvailable()) { + $call = call_user_func(static::$callbackApc, ini_get('apc.rfc1867_prefix') . $id); + if (is_array($call)) { + $status = $call + $status; + } + } elseif (static::isUploadProgressAvailable()) { + $call = call_user_func(static::$callbackUploadProgress, $id); + if (is_array($call)) { + $status = $call + $status; + $status['total'] = $status['bytes_total']; + $status['current'] = $status['bytes_uploaded']; + $status['rate'] = $status['speed_average']; + if ($status['total'] == $status['current']) { + $status['done'] = true; + } + } + } + + if (!is_array($call)) { + $status['done'] = true; + $status['message'] = 'Failure while retrieving the upload progress'; + } elseif (!empty($status['cancel_upload'])) { + $status['done'] = true; + $status['message'] = 'The upload has been canceled'; + } else { + $status['message'] = static::toByteString($status['current']) . " - " . static::toByteString($status['total']); + } + + $status['id'] = $id; + } + + if (isset($adapter) && isset($status['id'])) { + if ($adapter instanceof Adapter\AbstractAdapter) { + $adapter = new ProgressBar\ProgressBar($adapter, 0, $status['total'], $session); + } + + if (!($adapter instanceof ProgressBar\ProgressBar)) { + throw new Exception\RuntimeException('Unknown Adapter given'); + } + + if ($status['done']) { + $adapter->finish(); + } else { + $adapter->update($status['current'], $status['message']); + } + + $status['progress'] = $adapter; + } + + return $status; + } + + /** + * Checks the APC extension for progress information + * + * @return bool + */ + public static function isApcAvailable() + { + return (bool) ini_get('apc.enabled') && (bool) ini_get('apc.rfc1867') && is_callable(static::$callbackApc); + } + + /** + * Checks the UploadProgress extension for progress information + * + * @return bool + */ + public static function isUploadProgressAvailable() + { + return is_callable(static::$callbackUploadProgress); + } + + /** + * Prepare the $_FILES array to match the internal syntax of one file per entry + * + * @return Http + */ + protected function prepareFiles() + { + $this->files = array(); + foreach ($_FILES as $form => $content) { + if (is_array($content['name'])) { + foreach ($content as $param => $file) { + foreach ($file as $number => $target) { + $this->files[$form . '_' . $number . '_'][$param] = $target; + $this->files[$form]['multifiles'][$number] = $form . '_' . $number . '_'; + } + } + + $this->files[$form]['name'] = $form; + foreach ($this->files[$form]['multifiles'] as $key => $value) { + $this->files[$value]['options'] = $this->options; + $this->files[$value]['validated'] = false; + $this->files[$value]['received'] = false; + $this->files[$value]['filtered'] = false; + + $mimetype = $this->detectMimeType($this->files[$value]); + $this->files[$value]['type'] = $mimetype; + + $filesize = $this->detectFileSize($this->files[$value]); + $this->files[$value]['size'] = $filesize; + + if ($this->options['detectInfos']) { + $_FILES[$form]['type'][$key] = $mimetype; + $_FILES[$form]['size'][$key] = $filesize; + } + } + } else { + $this->files[$form] = $content; + $this->files[$form]['options'] = $this->options; + $this->files[$form]['validated'] = false; + $this->files[$form]['received'] = false; + $this->files[$form]['filtered'] = false; + + $mimetype = $this->detectMimeType($this->files[$form]); + $this->files[$form]['type'] = $mimetype; + + $filesize = $this->detectFileSize($this->files[$form]); + $this->files[$form]['size'] = $filesize; + + if ($this->options['detectInfos']) { + $_FILES[$form]['type'] = $mimetype; + $_FILES[$form]['size'] = $filesize; + } + } + } + + return $this; + } +} diff --git a/library/Zend/File/Transfer/Adapter/ValidatorPluginManager.php b/library/Zend/File/Transfer/Adapter/ValidatorPluginManager.php new file mode 100755 index 0000000000..f24f5d459c --- /dev/null +++ b/library/Zend/File/Transfer/Adapter/ValidatorPluginManager.php @@ -0,0 +1,36 @@ + 'filecount', + 'crc32' => 'filecrc32', + 'excludeextension' => 'fileexcludeextension', + 'excludemimetype' => 'fileexcludemimetype', + 'exists' => 'fileexists', + 'extension' => 'fileextension', + 'filessize' => 'filefilessize', + 'hash' => 'filehash', + 'imagesize' => 'fileimagesize', + 'iscompressed' => 'fileiscompressed', + 'isimage' => 'fileisimage', + 'md5' => 'filemd5', + 'mimetype' => 'filemimetype', + 'notexists' => 'filenotexists', + 'sha1' => 'filesha1', + 'size' => 'filesize', + 'upload' => 'fileupload', + 'wordcount' => 'filewordcount', + ); +} diff --git a/library/Zend/File/Transfer/Exception/BadMethodCallException.php b/library/Zend/File/Transfer/Exception/BadMethodCallException.php new file mode 100755 index 0000000000..0d47a905b6 --- /dev/null +++ b/library/Zend/File/Transfer/Exception/BadMethodCallException.php @@ -0,0 +1,17 @@ +setAdapter($adapter, $direction, $options); + } + + /** + * Sets a new adapter + * + * @param string $adapter Adapter to use + * @param bool $direction OPTIONAL False means Download, true means upload + * @param array $options OPTIONAL Options to set for this adapter + * @return Transfer + * @throws Exception\InvalidArgumentException + */ + public function setAdapter($adapter, $direction = false, $options = array()) + { + if (!is_string($adapter)) { + throw new Exception\InvalidArgumentException('Adapter must be a string'); + } + + if ($adapter[0] != '\\') { + $adapter = '\Zend\File\Transfer\Adapter\\' . ucfirst($adapter); + } + + $direction = (int) $direction; + $this->adapter[$direction] = new $adapter($options); + if (!$this->adapter[$direction] instanceof Adapter\AbstractAdapter) { + throw new Exception\InvalidArgumentException( + 'Adapter ' . $adapter . ' does not extend Zend\File\Transfer\Adapter\AbstractAdapter' + ); + } + + return $this; + } + + /** + * Returns all set adapters + * + * @param bool $direction On null, all directions are returned + * On false, download direction is returned + * On true, upload direction is returned + * @return array|Adapter\AbstractAdapter + */ + public function getAdapter($direction = null) + { + if ($direction === null) { + return $this->adapter; + } + + $direction = (int) $direction; + return $this->adapter[$direction]; + } + + /** + * Calls all methods from the adapter + * + * @param string $method Method to call + * @param array $options Options for this method + * @throws Exception\BadMethodCallException if unknown method + * @return mixed + */ + public function __call($method, array $options) + { + if (array_key_exists('direction', $options)) { + $direction = (int) $options['direction']; + } else { + $direction = 0; + } + + if (method_exists($this->adapter[$direction], $method)) { + return call_user_func_array(array($this->adapter[$direction], $method), $options); + } + + throw new Exception\BadMethodCallException("Unknown method '" . $method . "' called!"); + } +} diff --git a/library/Zend/File/composer.json b/library/Zend/File/composer.json new file mode 100755 index 0000000000..d3d86cea94 --- /dev/null +++ b/library/Zend/File/composer.json @@ -0,0 +1,36 @@ +{ + "name": "zendframework/zend-file", + "description": " ", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "file" + ], + "homepage": "https://github.com/zendframework/zf2", + "autoload": { + "psr-0": { + "Zend\\File\\": "" + } + }, + "target-dir": "Zend/File", + "require": { + "php": ">=5.3.23", + "zendframework/zend-stdlib": "self.version" + }, + "require-dev": { + "zendframework/zend-filter": "self.version", + "zendframework/zend-i18n": "self.version", + "zendframework/zend-validator": "self.version" + }, + "suggest": { + "zendframework/zend-filter": "Zend\\Filter component", + "zendframework/zend-i18n": "Zend\\I18n component", + "zendframework/zend-validator": "Zend\\Validator component" + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev", + "dev-develop": "2.4-dev" + } + } +} diff --git a/library/Zend/Filter/AbstractFilter.php b/library/Zend/Filter/AbstractFilter.php new file mode 100755 index 0000000000..a52bcfa739 --- /dev/null +++ b/library/Zend/Filter/AbstractFilter.php @@ -0,0 +1,96 @@ + $value) { + $setter = 'set' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key))); + if (method_exists($this, $setter)) { + $this->{$setter}($value); + } elseif (array_key_exists($key, $this->options)) { + $this->options[$key] = $value; + } else { + throw new Exception\InvalidArgumentException(sprintf( + 'The option "%s" does not have a matching %s setter method or options[%s] array key', + $key, $setter, $key + )); + } + } + return $this; + } + + /** + * Retrieve options representing object state + * + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * Invoke filter as a command + * + * Proxies to {@link filter()} + * + * @param mixed $value + * @throws Exception\ExceptionInterface If filtering $value is impossible + * @return mixed + */ + public function __invoke($value) + { + return $this->filter($value); + } + + /** + * @param mixed $options + * @return bool + */ + protected static function isOptions($options) + { + return (is_array($options) || $options instanceof Traversable); + } +} diff --git a/library/Zend/Filter/AbstractUnicode.php b/library/Zend/Filter/AbstractUnicode.php new file mode 100755 index 0000000000..685608f7d7 --- /dev/null +++ b/library/Zend/Filter/AbstractUnicode.php @@ -0,0 +1,59 @@ +options['encoding'] = $encoding; + return $this; + } + + /** + * Returns the set encoding + * + * @return string + */ + public function getEncoding() + { + if ($this->options['encoding'] === null && function_exists('mb_internal_encoding')) { + $this->options['encoding'] = mb_internal_encoding(); + } + + return $this->options['encoding']; + } +} diff --git a/library/Zend/Filter/BaseName.php b/library/Zend/Filter/BaseName.php new file mode 100755 index 0000000000..48807ca415 --- /dev/null +++ b/library/Zend/Filter/BaseName.php @@ -0,0 +1,33 @@ + 'boolean', + self::TYPE_INTEGER => 'integer', + self::TYPE_FLOAT => 'float', + self::TYPE_STRING => 'string', + self::TYPE_ZERO_STRING => 'zero', + self::TYPE_EMPTY_ARRAY => 'array', + self::TYPE_NULL => 'null', + self::TYPE_PHP => 'php', + self::TYPE_FALSE_STRING => 'false', + self::TYPE_LOCALIZED => 'localized', + self::TYPE_ALL => 'all', + ); + + /** + * @var array + */ + protected $options = array( + 'type' => self::TYPE_PHP, + 'casting' => true, + 'translations' => array(), + ); + + /** + * Constructor + * + * @param array|Traversable|int|null $typeOrOptions + * @param bool $casting + * @param array $translations + */ + public function __construct($typeOrOptions = null, $casting = true, $translations = array()) + { + if ($typeOrOptions !== null) { + if ($typeOrOptions instanceof Traversable) { + $typeOrOptions = ArrayUtils::iteratorToArray($typeOrOptions); + } + + if (is_array($typeOrOptions)) { + if (isset($typeOrOptions['type']) + || isset($typeOrOptions['casting']) + || isset($typeOrOptions['translations']) + ) { + $this->setOptions($typeOrOptions); + } else { + $this->setType($typeOrOptions); + $this->setCasting($casting); + $this->setTranslations($translations); + } + } else { + $this->setType($typeOrOptions); + $this->setCasting($casting); + $this->setTranslations($translations); + } + } + } + + /** + * Set boolean types + * + * @param int|array $type + * @throws Exception\InvalidArgumentException + * @return self + */ + public function setType($type = null) + { + if (is_array($type)) { + $detected = 0; + foreach ($type as $value) { + if (is_int($value)) { + $detected += $value; + } elseif (in_array($value, $this->constants)) { + $detected += array_search($value, $this->constants); + } + } + + $type = $detected; + } elseif (is_string($type) && in_array($type, $this->constants)) { + $type = array_search($type, $this->constants); + } + + if (!is_int($type) || ($type < 0) || ($type > self::TYPE_ALL)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Unknown type value "%s" (%s)', + $type, + gettype($type) + )); + } + + $this->options['type'] = $type; + return $this; + } + + /** + * Returns defined boolean types + * + * @return int + */ + public function getType() + { + return $this->options['type']; + } + + /** + * Set the working mode + * + * @param bool $flag When true this filter works like cast + * When false it recognises only true and false + * and all other values are returned as is + * @return self + */ + public function setCasting($flag = true) + { + $this->options['casting'] = (bool) $flag; + return $this; + } + + /** + * Returns the casting option + * + * @return bool + */ + public function getCasting() + { + return $this->options['casting']; + } + + /** + * @param array|Traversable $translations + * @throws Exception\InvalidArgumentException + * @return self + */ + public function setTranslations($translations) + { + if (!is_array($translations) && !$translations instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + '"%s" expects an array or Traversable; received "%s"', + __METHOD__, + (is_object($translations) ? get_class($translations) : gettype($translations)) + )); + } + + foreach ($translations as $message => $flag) { + $this->options['translations'][$message] = (bool) $flag; + } + + return $this; + } + + /** + * @return array + */ + public function getTranslations() + { + return $this->options['translations']; + } + + /** + * Defined by Zend\Filter\FilterInterface + * + * Returns a boolean representation of $value + * + * @param string $value + * @return string + */ + public function filter($value) + { + $type = $this->getType(); + $casting = $this->getCasting(); + + // LOCALIZED + if ($type >= self::TYPE_LOCALIZED) { + $type -= self::TYPE_LOCALIZED; + if (is_string($value)) { + if (isset($this->options['translations'][$value])) { + return (bool) $this->options['translations'][$value]; + } + } + } + + // FALSE_STRING ('false') + if ($type >= self::TYPE_FALSE_STRING) { + $type -= self::TYPE_FALSE_STRING; + if (is_string($value) && (strtolower($value) == 'false')) { + return false; + } + + if (!$casting && is_string($value) && (strtolower($value) == 'true')) { + return true; + } + } + + // NULL (null) + if ($type >= self::TYPE_NULL) { + $type -= self::TYPE_NULL; + if ($value === null) { + return false; + } + } + + // EMPTY_ARRAY (array()) + if ($type >= self::TYPE_EMPTY_ARRAY) { + $type -= self::TYPE_EMPTY_ARRAY; + if (is_array($value) && ($value == array())) { + return false; + } + } + + // ZERO_STRING ('0') + if ($type >= self::TYPE_ZERO_STRING) { + $type -= self::TYPE_ZERO_STRING; + if (is_string($value) && ($value == '0')) { + return false; + } + + if (!$casting && (is_string($value)) && ($value == '1')) { + return true; + } + } + + // STRING ('') + if ($type >= self::TYPE_STRING) { + $type -= self::TYPE_STRING; + if (is_string($value) && ($value == '')) { + return false; + } + } + + // FLOAT (0.0) + if ($type >= self::TYPE_FLOAT) { + $type -= self::TYPE_FLOAT; + if (is_float($value) && ($value == 0.0)) { + return false; + } + + if (!$casting && is_float($value) && ($value == 1.0)) { + return true; + } + } + + // INTEGER (0) + if ($type >= self::TYPE_INTEGER) { + $type -= self::TYPE_INTEGER; + if (is_int($value) && ($value == 0)) { + return false; + } + + if (!$casting && is_int($value) && ($value == 1)) { + return true; + } + } + + // BOOLEAN (false) + if ($type >= self::TYPE_BOOLEAN) { + $type -= self::TYPE_BOOLEAN; + if (is_bool($value)) { + return $value; + } + } + + if ($casting) { + return true; + } + + return $value; + } +} diff --git a/library/Zend/Filter/CONTRIBUTING.md b/library/Zend/Filter/CONTRIBUTING.md new file mode 100755 index 0000000000..e77f5d2d5b --- /dev/null +++ b/library/Zend/Filter/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# CONTRIBUTING + +Please don't open pull requests against this repository, please use https://github.com/zendframework/zf2. \ No newline at end of file diff --git a/library/Zend/Filter/Callback.php b/library/Zend/Filter/Callback.php new file mode 100755 index 0000000000..51392e1667 --- /dev/null +++ b/library/Zend/Filter/Callback.php @@ -0,0 +1,102 @@ + null, + 'callback_params' => array() + ); + + /** + * @param callable|array|Traversable $callbackOrOptions + * @param array $callbackParams + */ + public function __construct($callbackOrOptions, $callbackParams = array()) + { + if (is_callable($callbackOrOptions)) { + $this->setCallback($callbackOrOptions); + $this->setCallbackParams($callbackParams); + } else { + $this->setOptions($callbackOrOptions); + } + } + + /** + * Sets a new callback for this filter + * + * @param callable $callback + * @throws Exception\InvalidArgumentException + * @return self + */ + public function setCallback($callback) + { + if (!is_callable($callback)) { + throw new Exception\InvalidArgumentException( + 'Invalid parameter for callback: must be callable' + ); + } + + $this->options['callback'] = $callback; + return $this; + } + + /** + * Returns the set callback + * + * @return callable + */ + public function getCallback() + { + return $this->options['callback']; + } + + /** + * Sets parameters for the callback + * + * @param array $params + * @return self + */ + public function setCallbackParams($params) + { + $this->options['callback_params'] = (array) $params; + return $this; + } + + /** + * Get parameters for the callback + * + * @return array + */ + public function getCallbackParams() + { + return $this->options['callback_params']; + } + + /** + * Calls the filter per callback + * + * @param mixed $value Options for the set callable + * @return mixed Result from the filter which was called + */ + public function filter($value) + { + $params = (array) $this->options['callback_params']; + array_unshift($params, $value); + + return call_user_func_array($this->options['callback'], $params); + } +} diff --git a/library/Zend/Filter/Compress.php b/library/Zend/Filter/Compress.php new file mode 100755 index 0000000000..f6a49e779c --- /dev/null +++ b/library/Zend/Filter/Compress.php @@ -0,0 +1,210 @@ +setAdapter($options); + } elseif ($options instanceof Compress\CompressionAlgorithmInterface) { + $this->setAdapter($options); + } elseif (is_array($options)) { + $this->setOptions($options); + } + } + + /** + * Set filter setate + * + * @param array $options + * @throws Exception\InvalidArgumentException if options is not an array or Traversable + * @return self + */ + public function setOptions($options) + { + if (!is_array($options) && !$options instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + '"%s" expects an array or Traversable; received "%s"', + __METHOD__, + (is_object($options) ? get_class($options) : gettype($options)) + )); + } + + foreach ($options as $key => $value) { + if ($key == 'options') { + $key = 'adapterOptions'; + } + $method = 'set' . ucfirst($key); + if (method_exists($this, $method)) { + $this->$method($value); + } + } + return $this; + } + + /** + * Returns the current adapter, instantiating it if necessary + * + * @throws Exception\RuntimeException + * @throws Exception\InvalidArgumentException + * @return Compress\CompressionAlgorithmInterface + */ + public function getAdapter() + { + if ($this->adapter instanceof Compress\CompressionAlgorithmInterface) { + return $this->adapter; + } + + $adapter = $this->adapter; + $options = $this->getAdapterOptions(); + if (!class_exists($adapter)) { + $adapter = 'Zend\\Filter\\Compress\\' . ucfirst($adapter); + if (!class_exists($adapter)) { + throw new Exception\RuntimeException(sprintf( + '%s unable to load adapter; class "%s" not found', + __METHOD__, + $this->adapter + )); + } + } + + $this->adapter = new $adapter($options); + if (!$this->adapter instanceof Compress\CompressionAlgorithmInterface) { + throw new Exception\InvalidArgumentException("Compression adapter '" . $adapter . "' does not implement Zend\\Filter\\Compress\\CompressionAlgorithmInterface"); + } + return $this->adapter; + } + + /** + * Retrieve adapter name + * + * @return string + */ + public function getAdapterName() + { + return $this->getAdapter()->toString(); + } + + /** + * Sets compression adapter + * + * @param string|Compress\CompressionAlgorithmInterface $adapter Adapter to use + * @return self + * @throws Exception\InvalidArgumentException + */ + public function setAdapter($adapter) + { + if ($adapter instanceof Compress\CompressionAlgorithmInterface) { + $this->adapter = $adapter; + return $this; + } + if (!is_string($adapter)) { + throw new Exception\InvalidArgumentException('Invalid adapter provided; must be string or instance of Zend\\Filter\\Compress\\CompressionAlgorithmInterface'); + } + $this->adapter = $adapter; + + return $this; + } + + /** + * Retrieve adapter options + * + * @return array + */ + public function getAdapterOptions() + { + return $this->adapterOptions; + } + + /** + * Set adapter options + * + * @param array $options + * @return self + */ + public function setAdapterOptions(array $options) + { + $this->adapterOptions = $options; + return $this; + } + + /** + * Get individual or all options from underlying adapter + * + * @param null|string $option + * @return mixed + */ + public function getOptions($option = null) + { + $adapter = $this->getAdapter(); + return $adapter->getOptions($option); + } + + /** + * Calls adapter methods + * + * @param string $method Method to call + * @param string|array $options Options for this method + * @return mixed + * @throws Exception\BadMethodCallException + */ + public function __call($method, $options) + { + $adapter = $this->getAdapter(); + if (!method_exists($adapter, $method)) { + throw new Exception\BadMethodCallException("Unknown method '{$method}'"); + } + + return call_user_func_array(array($adapter, $method), $options); + } + + /** + * Defined by Zend\Filter\FilterInterface + * + * Compresses the content $value with the defined settings + * + * @param string $value Content to compress + * @return string The compressed content + */ + public function filter($value) + { + if (!is_string($value)) { + return $value; + } + + return $this->getAdapter()->compress($value); + } +} diff --git a/library/Zend/Filter/Compress/AbstractCompressionAlgorithm.php b/library/Zend/Filter/Compress/AbstractCompressionAlgorithm.php new file mode 100755 index 0000000000..b97e68cf12 --- /dev/null +++ b/library/Zend/Filter/Compress/AbstractCompressionAlgorithm.php @@ -0,0 +1,77 @@ +setOptions($options); + } + } + + /** + * Returns one or all set options + * + * @param string $option (Optional) Option to return + * @return mixed + */ + public function getOptions($option = null) + { + if ($option === null) { + return $this->options; + } + + if (!array_key_exists($option, $this->options)) { + return null; + } + + return $this->options[$option]; + } + + /** + * Sets all or one option + * + * @param array $options + * @return self + */ + public function setOptions(array $options) + { + foreach ($options as $key => $option) { + $method = 'set' . $key; + if (method_exists($this, $method)) { + $this->$method($option); + } + } + + return $this; + } +} diff --git a/library/Zend/Filter/Compress/Bz2.php b/library/Zend/Filter/Compress/Bz2.php new file mode 100755 index 0000000000..79716118b3 --- /dev/null +++ b/library/Zend/Filter/Compress/Bz2.php @@ -0,0 +1,170 @@ + Blocksize to use from 0-9 + * 'archive' => Archive to use + * ) + * + * @var array + */ + protected $options = array( + 'blocksize' => 4, + 'archive' => null, + ); + + /** + * Class constructor + * + * @param null|array|\Traversable $options (Optional) Options to set + * @throws Exception\ExtensionNotLoadedException if bz2 extension not loaded + */ + public function __construct($options = null) + { + if (!extension_loaded('bz2')) { + throw new Exception\ExtensionNotLoadedException('This filter needs the bz2 extension'); + } + parent::__construct($options); + } + + /** + * Returns the set blocksize + * + * @return int + */ + public function getBlocksize() + { + return $this->options['blocksize']; + } + + /** + * Sets a new blocksize + * + * @param int $blocksize + * @throws Exception\InvalidArgumentException + * @return self + */ + public function setBlocksize($blocksize) + { + if (($blocksize < 0) || ($blocksize > 9)) { + throw new Exception\InvalidArgumentException('Blocksize must be between 0 and 9'); + } + + $this->options['blocksize'] = (int) $blocksize; + return $this; + } + + /** + * Returns the set archive + * + * @return string + */ + public function getArchive() + { + return $this->options['archive']; + } + + /** + * Sets the archive to use for de-/compression + * + * @param string $archive Archive to use + * @return self + */ + public function setArchive($archive) + { + $this->options['archive'] = (string) $archive; + return $this; + } + + /** + * Compresses the given content + * + * @param string $content + * @return string + * @throws Exception\RuntimeException + */ + public function compress($content) + { + $archive = $this->getArchive(); + if (!empty($archive)) { + $file = bzopen($archive, 'w'); + if (!$file) { + throw new Exception\RuntimeException("Error opening the archive '" . $archive . "'"); + } + + bzwrite($file, $content); + bzclose($file); + $compressed = true; + } else { + $compressed = bzcompress($content, $this->getBlocksize()); + } + + if (is_int($compressed)) { + throw new Exception\RuntimeException('Error during compression'); + } + + return $compressed; + } + + /** + * Decompresses the given content + * + * @param string $content + * @return string + * @throws Exception\RuntimeException + */ + public function decompress($content) + { + $archive = $this->getArchive(); + + //check if there are null byte characters before doing a file_exists check + if (!strstr($content, "\0") && file_exists($content)) { + $archive = $content; + } + + if (file_exists($archive)) { + $file = bzopen($archive, 'r'); + if (!$file) { + throw new Exception\RuntimeException("Error opening the archive '" . $content . "'"); + } + + $compressed = bzread($file); + bzclose($file); + } else { + $compressed = bzdecompress($content); + } + + if (is_int($compressed)) { + throw new Exception\RuntimeException('Error during decompression'); + } + + return $compressed; + } + + /** + * Returns the adapter name + * + * @return string + */ + public function toString() + { + return 'Bz2'; + } +} diff --git a/library/Zend/Filter/Compress/CompressionAlgorithmInterface.php b/library/Zend/Filter/Compress/CompressionAlgorithmInterface.php new file mode 100755 index 0000000000..cf4e5f3cb1 --- /dev/null +++ b/library/Zend/Filter/Compress/CompressionAlgorithmInterface.php @@ -0,0 +1,39 @@ + Compression level 0-9 + * 'mode' => Compression mode, can be 'compress', 'deflate' + * 'archive' => Archive to use + * ) + * + * @var array + */ + protected $options = array( + 'level' => 9, + 'mode' => 'compress', + 'archive' => null, + ); + + /** + * Class constructor + * + * @param null|array|\Traversable $options (Optional) Options to set + * @throws Exception\ExtensionNotLoadedException if zlib extension not loaded + */ + public function __construct($options = null) + { + if (!extension_loaded('zlib')) { + throw new Exception\ExtensionNotLoadedException('This filter needs the zlib extension'); + } + parent::__construct($options); + } + + /** + * Returns the set compression level + * + * @return int + */ + public function getLevel() + { + return $this->options['level']; + } + + /** + * Sets a new compression level + * + * @param int $level + * @throws Exception\InvalidArgumentException + * @return self + */ + public function setLevel($level) + { + if (($level < 0) || ($level > 9)) { + throw new Exception\InvalidArgumentException('Level must be between 0 and 9'); + } + + $this->options['level'] = (int) $level; + return $this; + } + + /** + * Returns the set compression mode + * + * @return string + */ + public function getMode() + { + return $this->options['mode']; + } + + /** + * Sets a new compression mode + * + * @param string $mode Supported are 'compress', 'deflate' and 'file' + * @return self + * @throws Exception\InvalidArgumentException for invalid $mode value + */ + public function setMode($mode) + { + if (($mode != 'compress') && ($mode != 'deflate')) { + throw new Exception\InvalidArgumentException('Given compression mode not supported'); + } + + $this->options['mode'] = $mode; + return $this; + } + + /** + * Returns the set archive + * + * @return string + */ + public function getArchive() + { + return $this->options['archive']; + } + + /** + * Sets the archive to use for de-/compression + * + * @param string $archive Archive to use + * @return self + */ + public function setArchive($archive) + { + $this->options['archive'] = (string) $archive; + return $this; + } + + /** + * Compresses the given content + * + * @param string $content + * @return string + * @throws Exception\RuntimeException if unable to open archive or error during decompression + */ + public function compress($content) + { + $archive = $this->getArchive(); + if (!empty($archive)) { + $file = gzopen($archive, 'w' . $this->getLevel()); + if (!$file) { + throw new Exception\RuntimeException("Error opening the archive '" . $this->options['archive'] . "'"); + } + + gzwrite($file, $content); + gzclose($file); + $compressed = true; + } elseif ($this->options['mode'] == 'deflate') { + $compressed = gzdeflate($content, $this->getLevel()); + } else { + $compressed = gzcompress($content, $this->getLevel()); + } + + if (!$compressed) { + throw new Exception\RuntimeException('Error during compression'); + } + + return $compressed; + } + + /** + * Decompresses the given content + * + * @param string $content + * @return string + * @throws Exception\RuntimeException if unable to open archive or error during decompression + */ + public function decompress($content) + { + $archive = $this->getArchive(); + $mode = $this->getMode(); + + //check if there are null byte characters before doing a file_exists check + if (!strstr($content, "\0") && file_exists($content)) { + $archive = $content; + } + + if (file_exists($archive)) { + $handler = fopen($archive, "rb"); + if (!$handler) { + throw new Exception\RuntimeException("Error opening the archive '" . $archive . "'"); + } + + fseek($handler, -4, SEEK_END); + $packet = fread($handler, 4); + $bytes = unpack("V", $packet); + $size = end($bytes); + fclose($handler); + + $file = gzopen($archive, 'r'); + $compressed = gzread($file, $size); + gzclose($file); + } elseif ($mode == 'deflate') { + $compressed = gzinflate($content); + } else { + $compressed = gzuncompress($content); + } + + if ($compressed === false) { + throw new Exception\RuntimeException('Error during decompression'); + } + + return $compressed; + } + + /** + * Returns the adapter name + * + * @return string + */ + public function toString() + { + return 'Gz'; + } +} diff --git a/library/Zend/Filter/Compress/Lzf.php b/library/Zend/Filter/Compress/Lzf.php new file mode 100755 index 0000000000..ea4fd0c60d --- /dev/null +++ b/library/Zend/Filter/Compress/Lzf.php @@ -0,0 +1,75 @@ + Callback for compression + * 'archive' => Archive to use + * 'password' => Password to use + * 'target' => Target to write the files to + * ) + * + * @var array + */ + protected $options = array( + 'callback' => null, + 'archive' => null, + 'password' => null, + 'target' => '.', + ); + + /** + * Class constructor + * + * @param array $options (Optional) Options to set + * @throws Exception\ExtensionNotLoadedException if rar extension not loaded + */ + public function __construct($options = null) + { + if (!extension_loaded('rar')) { + throw new Exception\ExtensionNotLoadedException('This filter needs the rar extension'); + } + parent::__construct($options); + } + + /** + * Returns the set callback for compression + * + * @return string + */ + public function getCallback() + { + return $this->options['callback']; + } + + /** + * Sets the callback to use + * + * @param string $callback + * @return self + * @throws Exception\InvalidArgumentException if invalid callback provided + */ + public function setCallback($callback) + { + if (!is_callable($callback)) { + throw new Exception\InvalidArgumentException('Invalid callback provided'); + } + + $this->options['callback'] = $callback; + return $this; + } + + /** + * Returns the set archive + * + * @return string + */ + public function getArchive() + { + return $this->options['archive']; + } + + /** + * Sets the archive to use for de-/compression + * + * @param string $archive Archive to use + * @return self + */ + public function setArchive($archive) + { + $archive = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $archive); + $this->options['archive'] = (string) $archive; + + return $this; + } + + /** + * Returns the set password + * + * @return string + */ + public function getPassword() + { + return $this->options['password']; + } + + /** + * Sets the password to use + * + * @param string $password + * @return self + */ + public function setPassword($password) + { + $this->options['password'] = (string) $password; + return $this; + } + + /** + * Returns the set targetpath + * + * @return string + */ + public function getTarget() + { + return $this->options['target']; + } + + /** + * Sets the targetpath to use + * + * @param string $target + * @return self + * @throws Exception\InvalidArgumentException if specified target directory does not exist + */ + public function setTarget($target) + { + if (!file_exists(dirname($target))) { + throw new Exception\InvalidArgumentException("The directory '$target' does not exist"); + } + + $target = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, (string) $target); + $this->options['target'] = $target; + return $this; + } + + /** + * Compresses the given content + * + * @param string|array $content + * @return string + * @throws Exception\RuntimeException if no callback available, or error during compression + */ + public function compress($content) + { + $callback = $this->getCallback(); + if ($callback === null) { + throw new Exception\RuntimeException('No compression callback available'); + } + + $options = $this->getOptions(); + unset($options['callback']); + + $result = call_user_func($callback, $options, $content); + if ($result !== true) { + throw new Exception\RuntimeException('Error compressing the RAR Archive'); + } + + return $this->getArchive(); + } + + /** + * Decompresses the given content + * + * @param string $content + * @return bool + * @throws Exception\RuntimeException if archive not found, cannot be opened, + * or error during decompression + */ + public function decompress($content) + { + if (!file_exists($content)) { + throw new Exception\RuntimeException('RAR Archive not found'); + } + + $archive = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, realpath($content)); + $password = $this->getPassword(); + if ($password !== null) { + $archive = rar_open($archive, $password); + } else { + $archive = rar_open($archive); + } + + if (!$archive) { + throw new Exception\RuntimeException("Error opening the RAR Archive"); + } + + $target = $this->getTarget(); + if (!is_dir($target)) { + $target = dirname($target); + } + + $filelist = rar_list($archive); + if (!$filelist) { + throw new Exception\RuntimeException("Error reading the RAR Archive"); + } + + foreach ($filelist as $file) { + $file->extract($target); + } + + rar_close($archive); + return true; + } + + /** + * Returns the adapter name + * + * @return string + */ + public function toString() + { + return 'Rar'; + } +} diff --git a/library/Zend/Filter/Compress/Snappy.php b/library/Zend/Filter/Compress/Snappy.php new file mode 100755 index 0000000000..33a9c61627 --- /dev/null +++ b/library/Zend/Filter/Compress/Snappy.php @@ -0,0 +1,77 @@ + Archive to use + * 'target' => Target to write the files to + * ) + * + * @var array + */ + protected $options = array( + 'archive' => null, + 'target' => '.', + 'mode' => null, + ); + + /** + * Class constructor + * + * @param array $options (Optional) Options to set + * @throws Exception\ExtensionNotLoadedException if Archive_Tar component not available + */ + public function __construct($options = null) + { + if (!class_exists('Archive_Tar')) { + throw new Exception\ExtensionNotLoadedException( + 'This filter needs PEAR\'s Archive_Tar component. ' + . 'Ensure loading Archive_Tar (registering autoload or require_once)'); + } + + parent::__construct($options); + } + + /** + * Returns the set archive + * + * @return string + */ + public function getArchive() + { + return $this->options['archive']; + } + + /** + * Sets the archive to use for de-/compression + * + * @param string $archive Archive to use + * @return self + */ + public function setArchive($archive) + { + $archive = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, (string) $archive); + $this->options['archive'] = $archive; + + return $this; + } + + /** + * Returns the set target path + * + * @return string + */ + public function getTarget() + { + return $this->options['target']; + } + + /** + * Sets the target path to use + * + * @param string $target + * @return self + * @throws Exception\InvalidArgumentException if target path does not exist + */ + public function setTarget($target) + { + if (!file_exists(dirname($target))) { + throw new Exception\InvalidArgumentException("The directory '$target' does not exist"); + } + + $target = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, (string) $target); + $this->options['target'] = $target; + return $this; + } + + /** + * Returns the set compression mode + * + * @return string + */ + public function getMode() + { + return $this->options['mode']; + } + + /** + * Compression mode to use + * + * Either Gz or Bz2. + * + * @param string $mode + * @return self + * @throws Exception\InvalidArgumentException for invalid $mode values + * @throws Exception\ExtensionNotLoadedException if bz2 mode selected but extension not loaded + * @throws Exception\ExtensionNotLoadedException if gz mode selected but extension not loaded + */ + public function setMode($mode) + { + $mode = strtolower($mode); + if (($mode != 'bz2') && ($mode != 'gz')) { + throw new Exception\InvalidArgumentException("The mode '$mode' is unknown"); + } + + if (($mode == 'bz2') && (!extension_loaded('bz2'))) { + throw new Exception\ExtensionNotLoadedException('This mode needs the bz2 extension'); + } + + if (($mode == 'gz') && (!extension_loaded('zlib'))) { + throw new Exception\ExtensionNotLoadedException('This mode needs the zlib extension'); + } + + $this->options['mode'] = $mode; + return $this; + } + + /** + * Compresses the given content + * + * @param string $content + * @return string + * @throws Exception\RuntimeException if unable to create temporary file + * @throws Exception\RuntimeException if unable to create archive + */ + public function compress($content) + { + $archive = new Archive_Tar($this->getArchive(), $this->getMode()); + if (!file_exists($content)) { + $file = $this->getTarget(); + if (is_dir($file)) { + $file .= DIRECTORY_SEPARATOR . "tar.tmp"; + } + + $result = file_put_contents($file, $content); + if ($result === false) { + throw new Exception\RuntimeException('Error creating the temporary file'); + } + + $content = $file; + } + + if (is_dir($content)) { + // collect all file infos + foreach (new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($content, RecursiveDirectoryIterator::KEY_AS_PATHNAME), + RecursiveIteratorIterator::SELF_FIRST + ) as $directory => $info + ) { + if ($info->isFile()) { + $file[] = $directory; + } + } + + $content = $file; + } + + $result = $archive->create($content); + if ($result === false) { + throw new Exception\RuntimeException('Error creating the Tar archive'); + } + + return $this->getArchive(); + } + + /** + * Decompresses the given content + * + * @param string $content + * @return string + * @throws Exception\RuntimeException if unable to find archive + * @throws Exception\RuntimeException if error occurs decompressing archive + */ + public function decompress($content) + { + $archive = $this->getArchive(); + if (empty($archive) || !file_exists($archive)) { + throw new Exception\RuntimeException('Tar Archive not found'); + } + + $archive = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, realpath($content)); + $archive = new Archive_Tar($archive, $this->getMode()); + $target = $this->getTarget(); + if (!is_dir($target)) { + $target = dirname($target) . DIRECTORY_SEPARATOR; + } + + $result = $archive->extract($target); + if ($result === false) { + throw new Exception\RuntimeException('Error while extracting the Tar archive'); + } + + return $target; + } + + /** + * Returns the adapter name + * + * @return string + */ + public function toString() + { + return 'Tar'; + } +} diff --git a/library/Zend/Filter/Compress/Zip.php b/library/Zend/Filter/Compress/Zip.php new file mode 100755 index 0000000000..5a6f01a095 --- /dev/null +++ b/library/Zend/Filter/Compress/Zip.php @@ -0,0 +1,314 @@ + Archive to use + * 'password' => Password to use + * 'target' => Target to write the files to + * ) + * + * @var array + */ + protected $options = array( + 'archive' => null, + 'target' => null, + ); + + /** + * Class constructor + * + * @param null|array|\Traversable $options (Optional) Options to set + * @throws Exception\ExtensionNotLoadedException if zip extension not loaded + */ + public function __construct($options = null) + { + if (!extension_loaded('zip')) { + throw new Exception\ExtensionNotLoadedException('This filter needs the zip extension'); + } + parent::__construct($options); + } + + /** + * Returns the set archive + * + * @return string + */ + public function getArchive() + { + return $this->options['archive']; + } + + /** + * Sets the archive to use for de-/compression + * + * @param string $archive Archive to use + * @return self + */ + public function setArchive($archive) + { + $archive = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, (string) $archive); + $this->options['archive'] = $archive; + + return $this; + } + + /** + * Returns the set targetpath + * + * @return string + */ + public function getTarget() + { + return $this->options['target']; + } + + /** + * Sets the target to use + * + * @param string $target + * @throws Exception\InvalidArgumentException + * @return self + */ + public function setTarget($target) + { + if (!file_exists(dirname($target))) { + throw new Exception\InvalidArgumentException("The directory '$target' does not exist"); + } + + $target = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, (string) $target); + $this->options['target'] = $target; + return $this; + } + + /** + * Compresses the given content + * + * @param string $content + * @return string Compressed archive + * @throws Exception\RuntimeException if unable to open zip archive, or error during compression + */ + public function compress($content) + { + $zip = new ZipArchive(); + $res = $zip->open($this->getArchive(), ZipArchive::CREATE | ZipArchive::OVERWRITE); + + if ($res !== true) { + throw new Exception\RuntimeException($this->errorString($res)); + } + + if (file_exists($content)) { + $content = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, realpath($content)); + $basename = substr($content, strrpos($content, DIRECTORY_SEPARATOR) + 1); + if (is_dir($content)) { + $index = strrpos($content, DIRECTORY_SEPARATOR) + 1; + $content .= DIRECTORY_SEPARATOR; + $stack = array($content); + while (!empty($stack)) { + $current = array_pop($stack); + $files = array(); + + $dir = dir($current); + while (false !== ($node = $dir->read())) { + if (($node == '.') || ($node == '..')) { + continue; + } + + if (is_dir($current . $node)) { + array_push($stack, $current . $node . DIRECTORY_SEPARATOR); + } + + if (is_file($current . $node)) { + $files[] = $node; + } + } + + $local = substr($current, $index); + $zip->addEmptyDir(substr($local, 0, -1)); + + foreach ($files as $file) { + $zip->addFile($current . $file, $local . $file); + if ($res !== true) { + throw new Exception\RuntimeException($this->errorString($res)); + } + } + } + } else { + $res = $zip->addFile($content, $basename); + if ($res !== true) { + throw new Exception\RuntimeException($this->errorString($res)); + } + } + } else { + $file = $this->getTarget(); + if (!is_dir($file)) { + $file = basename($file); + } else { + $file = "zip.tmp"; + } + + $res = $zip->addFromString($file, $content); + if ($res !== true) { + throw new Exception\RuntimeException($this->errorString($res)); + } + } + + $zip->close(); + return $this->options['archive']; + } + + /** + * Decompresses the given content + * + * @param string $content + * @return string + * @throws Exception\RuntimeException If archive file not found, target directory not found, + * or error during decompression + */ + public function decompress($content) + { + $archive = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, realpath($content)); + + if (empty($archive) || !file_exists($archive)) { + throw new Exception\RuntimeException('ZIP Archive not found'); + } + + $zip = new ZipArchive(); + $res = $zip->open($archive); + + $target = $this->getTarget(); + if (!empty($target) && !is_dir($target)) { + $target = dirname($target); + } + + if (!empty($target)) { + $target = rtrim($target, '/\\') . DIRECTORY_SEPARATOR; + } + + if (empty($target) || !is_dir($target)) { + throw new Exception\RuntimeException('No target for ZIP decompression set'); + } + + if ($res !== true) { + throw new Exception\RuntimeException($this->errorString($res)); + } + + $res = $zip->extractTo($target); + if ($res !== true) { + throw new Exception\RuntimeException($this->errorString($res)); + } + + $zip->close(); + return $target; + } + + /** + * Returns the proper string based on the given error constant + * + * @param string $error + * @return string + */ + public function errorString($error) + { + switch ($error) { + case ZipArchive::ER_MULTIDISK : + return 'Multidisk ZIP Archives not supported'; + + case ZipArchive::ER_RENAME : + return 'Failed to rename the temporary file for ZIP'; + + case ZipArchive::ER_CLOSE : + return 'Failed to close the ZIP Archive'; + + case ZipArchive::ER_SEEK : + return 'Failure while seeking the ZIP Archive'; + + case ZipArchive::ER_READ : + return 'Failure while reading the ZIP Archive'; + + case ZipArchive::ER_WRITE : + return 'Failure while writing the ZIP Archive'; + + case ZipArchive::ER_CRC : + return 'CRC failure within the ZIP Archive'; + + case ZipArchive::ER_ZIPCLOSED : + return 'ZIP Archive already closed'; + + case ZipArchive::ER_NOENT : + return 'No such file within the ZIP Archive'; + + case ZipArchive::ER_EXISTS : + return 'ZIP Archive already exists'; + + case ZipArchive::ER_OPEN : + return 'Can not open ZIP Archive'; + + case ZipArchive::ER_TMPOPEN : + return 'Failure creating temporary ZIP Archive'; + + case ZipArchive::ER_ZLIB : + return 'ZLib Problem'; + + case ZipArchive::ER_MEMORY : + return 'Memory allocation problem while working on a ZIP Archive'; + + case ZipArchive::ER_CHANGED : + return 'ZIP Entry has been changed'; + + case ZipArchive::ER_COMPNOTSUPP : + return 'Compression method not supported within ZLib'; + + case ZipArchive::ER_EOF : + return 'Premature EOF within ZIP Archive'; + + case ZipArchive::ER_INVAL : + return 'Invalid argument for ZLIB'; + + case ZipArchive::ER_NOZIP : + return 'Given file is no zip archive'; + + case ZipArchive::ER_INTERNAL : + return 'Internal error while working on a ZIP Archive'; + + case ZipArchive::ER_INCONS : + return 'Inconsistent ZIP archive'; + + case ZipArchive::ER_REMOVE : + return 'Can not remove ZIP Archive'; + + case ZipArchive::ER_DELETED : + return 'ZIP Entry has been deleted'; + + default : + return 'Unknown error within ZIP Archive'; + } + } + + /** + * Returns the adapter name + * + * @return string + */ + public function toString() + { + return 'Zip'; + } +} diff --git a/library/Zend/Filter/DateTimeFormatter.php b/library/Zend/Filter/DateTimeFormatter.php new file mode 100755 index 0000000000..b24897b2f3 --- /dev/null +++ b/library/Zend/Filter/DateTimeFormatter.php @@ -0,0 +1,96 @@ +setOptions($options); + } + } + + /** + * Set the format string accepted by date() to use when formatting a string + * + * @param string $format + * @return self + */ + public function setFormat($format) + { + $this->format = $format; + + return $this; + } + + /** + * Filter a datetime string by normalizing it to the filters specified format + * + * @param DateTime|string|integer $value + * @throws Exception\InvalidArgumentException + * @return string + */ + public function filter($value) + { + try { + $result = $this->normalizeDateTime($value); + } catch (\Exception $e) { + // DateTime threw an exception, an invalid date string was provided + throw new Exception\InvalidArgumentException('Invalid date string provided', $e->getCode(), $e); + } + + if ($result === false) { + return $value; + } + + return $result; + } + + /** + * Normalize the provided value to a formatted string + * + * @param string|int|DateTime $value + * @return string + */ + protected function normalizeDateTime($value) + { + if ($value === '' || $value === null) { + return $value; + } + + if (!is_string($value) && !is_int($value) && !$value instanceof DateTime) { + return $value; + } + + if (is_int($value)) { + //timestamp + $value = new DateTime('@' . $value); + } elseif (!$value instanceof DateTime) { + $value = new DateTime($value); + } + + return $value->format($this->format); + } +} diff --git a/library/Zend/Filter/Decompress.php b/library/Zend/Filter/Decompress.php new file mode 100755 index 0000000000..3489c70c16 --- /dev/null +++ b/library/Zend/Filter/Decompress.php @@ -0,0 +1,46 @@ +filter($value); + } + + /** + * Defined by FilterInterface + * + * Decompresses the content $value with the defined settings + * + * @param string $value Content to decompress + * @return string The decompressed content + */ + public function filter($value) + { + if (!is_string($value) && $value !== null) { + return $value; + } + + return $this->getAdapter()->decompress($value); + } +} diff --git a/library/Zend/Filter/Decrypt.php b/library/Zend/Filter/Decrypt.php new file mode 100755 index 0000000000..9c8e8492bb --- /dev/null +++ b/library/Zend/Filter/Decrypt.php @@ -0,0 +1,33 @@ +adapter->decrypt($value); + } +} diff --git a/library/Zend/Filter/Digits.php b/library/Zend/Filter/Digits.php new file mode 100755 index 0000000000..c5e856fbd5 --- /dev/null +++ b/library/Zend/Filter/Digits.php @@ -0,0 +1,49 @@ +setAdapter($options); + } + + /** + * Returns the name of the set adapter + * @todo inconsitent: get adapter should return the adapter and not the name + * + * @return string + */ + public function getAdapter() + { + return $this->adapter->toString(); + } + + /** + * Sets new encryption options + * + * @param string|array $options (Optional) Encryption options + * @return self + * @throws Exception\DomainException + * @throws Exception\InvalidArgumentException + */ + public function setAdapter($options = null) + { + if (is_string($options)) { + $adapter = $options; + } elseif (isset($options['adapter'])) { + $adapter = $options['adapter']; + unset($options['adapter']); + } else { + $adapter = 'BlockCipher'; + } + + if (!is_array($options)) { + $options = array(); + } + + if (class_exists('Zend\Filter\Encrypt\\' . ucfirst($adapter))) { + $adapter = 'Zend\Filter\Encrypt\\' . ucfirst($adapter); + } elseif (!class_exists($adapter)) { + throw new Exception\DomainException( + sprintf('%s expects a valid registry class name; received "%s", which did not resolve', + __METHOD__, + $adapter + )); + } + + $this->adapter = new $adapter($options); + if (!$this->adapter instanceof Encrypt\EncryptionAlgorithmInterface) { + throw new Exception\InvalidArgumentException( + "Encoding adapter '" . $adapter + . "' does not implement Zend\\Filter\\Encrypt\\EncryptionAlgorithmInterface"); + } + + return $this; + } + + /** + * Calls adapter methods + * + * @param string $method Method to call + * @param string|array $options Options for this method + * @return mixed + * @throws Exception\BadMethodCallException + */ + public function __call($method, $options) + { + $part = substr($method, 0, 3); + if ((($part != 'get') && ($part != 'set')) || !method_exists($this->adapter, $method)) { + throw new Exception\BadMethodCallException("Unknown method '{$method}'"); + } + + return call_user_func_array(array($this->adapter, $method), $options); + } + + /** + * Defined by Zend\Filter\Filter + * + * Encrypts the content $value with the defined settings + * + * @param string $value Content to encrypt + * @return string The encrypted content + */ + public function filter($value) + { + if (!is_string($value)) { + return $value; + } + + return $this->adapter->encrypt($value); + } +} diff --git a/library/Zend/Filter/Encrypt/BlockCipher.php b/library/Zend/Filter/Encrypt/BlockCipher.php new file mode 100755 index 0000000000..5b7c13664c --- /dev/null +++ b/library/Zend/Filter/Encrypt/BlockCipher.php @@ -0,0 +1,288 @@ + encryption key string + * 'key_iteration' => the number of iterations for the PBKDF2 key generation + * 'algorithm => cipher algorithm to use + * 'hash' => algorithm to use for the authentication + * 'vector' => initialization vector + * ) + */ + protected $encryption = array( + 'key_iteration' => 5000, + 'algorithm' => 'aes', + 'hash' => 'sha256', + ); + + /** + * BlockCipher + * + * @var BlockCipher + */ + protected $blockCipher; + + /** + * Internal compression + * + * @var array + */ + protected $compression; + + /** + * Class constructor + * + * @param string|array|Traversable $options Encryption Options + * @throws Exception\RuntimeException + * @throws Exception\InvalidArgumentException + */ + public function __construct($options) + { + try { + $this->blockCipher = CryptBlockCipher::factory('mcrypt', $this->encryption); + } catch (SymmetricException\RuntimeException $e) { + throw new Exception\RuntimeException('The BlockCipher cannot be used without the Mcrypt extension'); + } + + if ($options instanceof Traversable) { + $options = ArrayUtils::iteratorToArray($options); + } elseif (is_string($options)) { + $options = array('key' => $options); + } elseif (!is_array($options)) { + throw new Exception\InvalidArgumentException('Invalid options argument provided to filter'); + } + + if (array_key_exists('compression', $options)) { + $this->setCompression($options['compression']); + unset($options['compress']); + } + + $this->setEncryption($options); + } + + /** + * Returns the set encryption options + * + * @return array + */ + public function getEncryption() + { + return $this->encryption; + } + + /** + * Sets new encryption options + * + * @param string|array $options Encryption options + * @return self + * @throws Exception\InvalidArgumentException + */ + public function setEncryption($options) + { + if (is_string($options)) { + $this->blockCipher->setKey($options); + $this->encryption['key'] = $options; + return $this; + } + + if (!is_array($options)) { + throw new Exception\InvalidArgumentException('Invalid options argument provided to filter'); + } + + $options = $options + $this->encryption; + + if (isset($options['key'])) { + $this->blockCipher->setKey($options['key']); + } + + if (isset($options['algorithm'])) { + try { + $this->blockCipher->setCipherAlgorithm($options['algorithm']); + } catch (CryptException\InvalidArgumentException $e) { + throw new Exception\InvalidArgumentException("The algorithm '{$options['algorithm']}' is not supported"); + } + } + + if (isset($options['hash'])) { + try { + $this->blockCipher->setHashAlgorithm($options['hash']); + } catch (CryptException\InvalidArgumentException $e) { + throw new Exception\InvalidArgumentException("The algorithm '{$options['hash']}' is not supported"); + } + } + + if (isset($options['vector'])) { + $this->setVector($options['vector']); + } + + if (isset($options['key_iteration'])) { + $this->blockCipher->setKeyIteration($options['key_iteration']); + } + + $this->encryption = $options; + + return $this; + } + + /** + * Returns the initialization vector + * + * @return string + */ + public function getVector() + { + return $this->encryption['vector']; + } + + /** + * Set the inizialization vector + * + * @param string $vector + * @return self + * @throws Exception\InvalidArgumentException + */ + public function setVector($vector) + { + try { + $this->blockCipher->setSalt($vector); + } catch (CryptException\InvalidArgumentException $e) { + throw new Exception\InvalidArgumentException($e->getMessage()); + } + $this->encryption['vector'] = $vector; + return $this; + } + + /** + * Set the encryption key + * + * @param string $key + * @return self + * @throws Exception\InvalidArgumentException + */ + public function setKey($key) + { + try { + $this->blockCipher->setKey($key); + } catch (CryptException\InvalidArgumentException $e) { + throw new Exception\InvalidArgumentException($e->getMessage()); + } + $this->encryption['key'] = $key; + return $this; + } + + /** + * Get the encryption key + * + * @return string + */ + public function getKey() + { + return $this->encryption['key']; + } + + /** + * Returns the compression + * + * @return array + */ + public function getCompression() + { + return $this->compression; + } + + /** + * Sets an internal compression for values to encrypt + * + * @param string|array $compression + * @return self + */ + public function setCompression($compression) + { + if (is_string($this->compression)) { + $compression = array('adapter' => $compression); + } + + $this->compression = $compression; + return $this; + } + + /** + * Defined by Zend\Filter\FilterInterface + * + * Encrypts $value with the defined settings + * + * @param string $value The content to encrypt + * @throws Exception\InvalidArgumentException + * @return string The encrypted content + */ + public function encrypt($value) + { + // compress prior to encryption + if (!empty($this->compression)) { + $compress = new Compress($this->compression); + $value = $compress($value); + } + + try { + $encrypted = $this->blockCipher->encrypt($value); + } catch (CryptException\InvalidArgumentException $e) { + throw new Exception\InvalidArgumentException($e->getMessage()); + } + return $encrypted; + } + + /** + * Defined by Zend\Filter\FilterInterface + * + * Decrypts $value with the defined settings + * + * @param string $value Content to decrypt + * @return string The decrypted content + */ + public function decrypt($value) + { + $decrypted = $this->blockCipher->decrypt($value); + + // decompress after decryption + if (!empty($this->compression)) { + $decompress = new Decompress($this->compression); + $decrypted = $decompress($decrypted); + } + + return $decrypted; + } + + /** + * Returns the adapter name + * + * @return string + */ + public function toString() + { + return 'BlockCipher'; + } +} diff --git a/library/Zend/Filter/Encrypt/EncryptionAlgorithmInterface.php b/library/Zend/Filter/Encrypt/EncryptionAlgorithmInterface.php new file mode 100755 index 0000000000..faf0c518cc --- /dev/null +++ b/library/Zend/Filter/Encrypt/EncryptionAlgorithmInterface.php @@ -0,0 +1,39 @@ + public keys + * 'private' => private keys + * 'envelope' => resulting envelope keys + * ) + */ + protected $keys = array( + 'public' => array(), + 'private' => array(), + 'envelope' => array(), + ); + + /** + * Internal passphrase + * + * @var string + */ + protected $passphrase; + + /** + * Internal compression + * + * @var array + */ + protected $compression; + + /** + * Internal create package + * + * @var bool + */ + protected $package = false; + + /** + * Class constructor + * Available options + * 'public' => public key + * 'private' => private key + * 'envelope' => envelope key + * 'passphrase' => passphrase + * 'compression' => compress value with this compression adapter + * 'package' => pack envelope keys into encrypted string, simplifies decryption + * + * @param string|array|Traversable $options Options for this adapter + * @throws Exception\ExtensionNotLoadedException + */ + public function __construct($options = array()) + { + if (!extension_loaded('openssl')) { + throw new Exception\ExtensionNotLoadedException('This filter needs the openssl extension'); + } + + if ($options instanceof Traversable) { + $options = ArrayUtils::iteratorToArray($options); + } + + if (!is_array($options)) { + $options = array('public' => $options); + } + + if (array_key_exists('passphrase', $options)) { + $this->setPassphrase($options['passphrase']); + unset($options['passphrase']); + } + + if (array_key_exists('compression', $options)) { + $this->setCompression($options['compression']); + unset($options['compress']); + } + + if (array_key_exists('package', $options)) { + $this->setPackage($options['package']); + unset($options['package']); + } + + $this->_setKeys($options); + } + + /** + * Sets the encryption keys + * + * @param string|array $keys Key with type association + * @return self + * @throws Exception\InvalidArgumentException + */ + protected function _setKeys($keys) + { + if (!is_array($keys)) { + throw new Exception\InvalidArgumentException('Invalid options argument provided to filter'); + } + + foreach ($keys as $type => $key) { + if (is_file($key) and is_readable($key)) { + $file = fopen($key, 'r'); + $cert = fread($file, 8192); + fclose($file); + } else { + $cert = $key; + $key = count($this->keys[$type]); + } + + switch ($type) { + case 'public': + $test = openssl_pkey_get_public($cert); + if ($test === false) { + throw new Exception\InvalidArgumentException("Public key '{$cert}' not valid"); + } + + openssl_free_key($test); + $this->keys['public'][$key] = $cert; + break; + case 'private': + $test = openssl_pkey_get_private($cert, $this->passphrase); + if ($test === false) { + throw new Exception\InvalidArgumentException("Private key '{$cert}' not valid"); + } + + openssl_free_key($test); + $this->keys['private'][$key] = $cert; + break; + case 'envelope': + $this->keys['envelope'][$key] = $cert; + break; + default: + break; + } + } + + return $this; + } + + /** + * Returns all public keys + * + * @return array + */ + public function getPublicKey() + { + $key = $this->keys['public']; + return $key; + } + + /** + * Sets public keys + * + * @param string|array $key Public keys + * @return self + */ + public function setPublicKey($key) + { + if (is_array($key)) { + foreach ($key as $type => $option) { + if ($type !== 'public') { + $key['public'] = $option; + unset($key[$type]); + } + } + } else { + $key = array('public' => $key); + } + + return $this->_setKeys($key); + } + + /** + * Returns all private keys + * + * @return array + */ + public function getPrivateKey() + { + $key = $this->keys['private']; + return $key; + } + + /** + * Sets private keys + * + * @param string $key Private key + * @param string $passphrase + * @return self + */ + public function setPrivateKey($key, $passphrase = null) + { + if (is_array($key)) { + foreach ($key as $type => $option) { + if ($type !== 'private') { + $key['private'] = $option; + unset($key[$type]); + } + } + } else { + $key = array('private' => $key); + } + + if ($passphrase !== null) { + $this->setPassphrase($passphrase); + } + + return $this->_setKeys($key); + } + + /** + * Returns all envelope keys + * + * @return array + */ + public function getEnvelopeKey() + { + $key = $this->keys['envelope']; + return $key; + } + + /** + * Sets envelope keys + * + * @param string|array $key Envelope keys + * @return self + */ + public function setEnvelopeKey($key) + { + if (is_array($key)) { + foreach ($key as $type => $option) { + if ($type !== 'envelope') { + $key['envelope'] = $option; + unset($key[$type]); + } + } + } else { + $key = array('envelope' => $key); + } + + return $this->_setKeys($key); + } + + /** + * Returns the passphrase + * + * @return string + */ + public function getPassphrase() + { + return $this->passphrase; + } + + /** + * Sets a new passphrase + * + * @param string $passphrase + * @return self + */ + public function setPassphrase($passphrase) + { + $this->passphrase = $passphrase; + return $this; + } + + /** + * Returns the compression + * + * @return array + */ + public function getCompression() + { + return $this->compression; + } + + /** + * Sets an internal compression for values to encrypt + * + * @param string|array $compression + * @return self + */ + public function setCompression($compression) + { + if (is_string($this->compression)) { + $compression = array('adapter' => $compression); + } + + $this->compression = $compression; + return $this; + } + + /** + * Returns if header should be packaged + * + * @return bool + */ + public function getPackage() + { + return $this->package; + } + + /** + * Sets if the envelope keys should be included in the encrypted value + * + * @param bool $package + * @return self + */ + public function setPackage($package) + { + $this->package = (bool) $package; + return $this; + } + + /** + * Encrypts $value with the defined settings + * Note that you also need the "encrypted" keys to be able to decrypt + * + * @param string $value Content to encrypt + * @return string The encrypted content + * @throws Exception\RuntimeException + */ + public function encrypt($value) + { + $encrypted = array(); + $encryptedkeys = array(); + + if (count($this->keys['public']) == 0) { + throw new Exception\RuntimeException('Openssl can not encrypt without public keys'); + } + + $keys = array(); + $fingerprints = array(); + $count = -1; + foreach ($this->keys['public'] as $key => $cert) { + $keys[$key] = openssl_pkey_get_public($cert); + if ($this->package) { + $details = openssl_pkey_get_details($keys[$key]); + if ($details === false) { + $details = array('key' => 'ZendFramework'); + } + + ++$count; + $fingerprints[$count] = md5($details['key']); + } + } + + // compress prior to encryption + if (!empty($this->compression)) { + $compress = new Compress($this->compression); + $value = $compress($value); + } + + $crypt = openssl_seal($value, $encrypted, $encryptedkeys, $keys); + foreach ($keys as $key) { + openssl_free_key($key); + } + + if ($crypt === false) { + throw new Exception\RuntimeException('Openssl was not able to encrypt your content with the given options'); + } + + $this->keys['envelope'] = $encryptedkeys; + + // Pack data and envelope keys into single string + if ($this->package) { + $header = pack('n', count($this->keys['envelope'])); + foreach ($this->keys['envelope'] as $key => $envKey) { + $header .= pack('H32n', $fingerprints[$key], strlen($envKey)) . $envKey; + } + + $encrypted = $header . $encrypted; + } + + return $encrypted; + } + + /** + * Defined by Zend\Filter\FilterInterface + * + * Decrypts $value with the defined settings + * + * @param string $value Content to decrypt + * @return string The decrypted content + * @throws Exception\RuntimeException + */ + public function decrypt($value) + { + $decrypted = ""; + $envelope = current($this->getEnvelopeKey()); + + if (count($this->keys['private']) !== 1) { + throw new Exception\RuntimeException('Please give a private key for decryption with Openssl'); + } + + if (!$this->package && empty($envelope)) { + throw new Exception\RuntimeException('Please give an envelope key for decryption with Openssl'); + } + + foreach ($this->keys['private'] as $cert) { + $keys = openssl_pkey_get_private($cert, $this->getPassphrase()); + } + + if ($this->package) { + $details = openssl_pkey_get_details($keys); + if ($details !== false) { + $fingerprint = md5($details['key']); + } else { + $fingerprint = md5("ZendFramework"); + } + + $count = unpack('ncount', $value); + $count = $count['count']; + $length = 2; + for ($i = $count; $i > 0; --$i) { + $header = unpack('H32print/nsize', substr($value, $length, 18)); + $length += 18; + if ($header['print'] == $fingerprint) { + $envelope = substr($value, $length, $header['size']); + } + + $length += $header['size']; + } + + // remainder of string is the value to decrypt + $value = substr($value, $length); + } + + $crypt = openssl_open($value, $decrypted, $envelope, $keys); + openssl_free_key($keys); + + if ($crypt === false) { + throw new Exception\RuntimeException('Openssl was not able to decrypt you content with the given options'); + } + + // decompress after decryption + if (!empty($this->compression)) { + $decompress = new Decompress($this->compression); + $decrypted = $decompress($decrypted); + } + + return $decrypted; + } + + /** + * Returns the adapter name + * + * @return string + */ + public function toString() + { + return 'Openssl'; + } +} diff --git a/library/Zend/Filter/Exception/BadMethodCallException.php b/library/Zend/Filter/Exception/BadMethodCallException.php new file mode 100755 index 0000000000..ae0d3f843c --- /dev/null +++ b/library/Zend/Filter/Exception/BadMethodCallException.php @@ -0,0 +1,14 @@ +filename; + } + + /** + * Sets the new filename where the content will be stored + * + * @param string $filename (Optional) New filename to set + * @return self + */ + public function setFilename($filename = null) + { + $this->filename = $filename; + return $this; + } + + /** + * Defined by Zend\Filter\FilterInterface + * + * Decrypts the file $value with the defined settings + * + * @param string|array $value Full path of file to change or $_FILES data array + * @return string|array The filename which has been set + * @throws Exception\InvalidArgumentException + * @throws Exception\RuntimeException + */ + public function filter($value) + { + if (!is_scalar($value) && !is_array($value)) { + return $value; + } + + // An uploaded file? Retrieve the 'tmp_name' + $isFileUpload = false; + if (is_array($value)) { + if (!isset($value['tmp_name'])) { + return $value; + } + + $isFileUpload = true; + $uploadData = $value; + $value = $value['tmp_name']; + } + + + if (!file_exists($value)) { + throw new Exception\InvalidArgumentException("File '$value' not found"); + } + + if (!isset($this->filename)) { + $this->filename = $value; + } + + if (file_exists($this->filename) and !is_writable($this->filename)) { + throw new Exception\RuntimeException("File '{$this->filename}' is not writable"); + } + + $content = file_get_contents($value); + if (!$content) { + throw new Exception\RuntimeException("Problem while reading file '$value'"); + } + + $decrypted = parent::filter($content); + $result = file_put_contents($this->filename, $decrypted); + + if (!$result) { + throw new Exception\RuntimeException("Problem while writing file '{$this->filename}'"); + } + + if ($isFileUpload) { + $uploadData['tmp_name'] = $this->filename; + return $uploadData; + } + return $this->filename; + } +} diff --git a/library/Zend/Filter/File/Encrypt.php b/library/Zend/Filter/File/Encrypt.php new file mode 100755 index 0000000000..9e32b4ce74 --- /dev/null +++ b/library/Zend/Filter/File/Encrypt.php @@ -0,0 +1,107 @@ +filename; + } + + /** + * Sets the new filename where the content will be stored + * + * @param string $filename (Optional) New filename to set + * @return self + */ + public function setFilename($filename = null) + { + $this->filename = $filename; + return $this; + } + + /** + * Defined by Zend\Filter\Filter + * + * Encrypts the file $value with the defined settings + * + * @param string|array $value Full path of file to change or $_FILES data array + * @return string|array The filename which has been set, or false when there were errors + * @throws Exception\InvalidArgumentException + * @throws Exception\RuntimeException + */ + public function filter($value) + { + if (!is_scalar($value) && !is_array($value)) { + return $value; + } + + // An uploaded file? Retrieve the 'tmp_name' + $isFileUpload = false; + if (is_array($value)) { + if (!isset($value['tmp_name'])) { + return $value; + } + + $isFileUpload = true; + $uploadData = $value; + $value = $value['tmp_name']; + } + + if (!file_exists($value)) { + throw new Exception\InvalidArgumentException("File '$value' not found"); + } + + if (!isset($this->filename)) { + $this->filename = $value; + } + + if (file_exists($this->filename) and !is_writable($this->filename)) { + throw new Exception\RuntimeException("File '{$this->filename}' is not writable"); + } + + $content = file_get_contents($value); + if (!$content) { + throw new Exception\RuntimeException("Problem while reading file '$value'"); + } + + $encrypted = parent::filter($content); + $result = file_put_contents($this->filename, $encrypted); + + if (!$result) { + throw new Exception\RuntimeException("Problem while writing file '{$this->filename}'"); + } + + if ($isFileUpload) { + $uploadData['tmp_name'] = $this->filename; + return $uploadData; + } + return $this->filename; + } +} diff --git a/library/Zend/Filter/File/LowerCase.php b/library/Zend/Filter/File/LowerCase.php new file mode 100755 index 0000000000..15d31482e7 --- /dev/null +++ b/library/Zend/Filter/File/LowerCase.php @@ -0,0 +1,70 @@ + Source filename or directory which will be renamed + * 'target' => Target filename or directory, the new name of the source file + * 'overwrite' => Shall existing files be overwritten ? + * 'randomize' => Shall target files have a random postfix attached? + * + * @param string|array|Traversable $options Target file or directory to be renamed + * @throws Exception\InvalidArgumentException + */ + public function __construct($options) + { + if ($options instanceof Traversable) { + $options = ArrayUtils::iteratorToArray($options); + } elseif (is_string($options)) { + $options = array('target' => $options); + } elseif (!is_array($options)) { + throw new Exception\InvalidArgumentException( + 'Invalid options argument provided to filter' + ); + } + + $this->setFile($options); + } + + /** + * Returns the files to rename and their new name and location + * + * @return array + */ + public function getFile() + { + return $this->files; + } + + /** + * Sets a new file or directory as target, deleting existing ones + * + * Array accepts the following keys: + * 'source' => Source filename or directory which will be renamed + * 'target' => Target filename or directory, the new name of the sourcefile + * 'overwrite' => Shall existing files be overwritten? + * 'randomize' => Shall target files have a random postfix attached? + * + * @param string|array $options Old file or directory to be rewritten + * @return self + */ + public function setFile($options) + { + $this->files = array(); + $this->addFile($options); + + return $this; + } + + /** + * Adds a new file or directory as target to the existing ones + * + * Array accepts the following keys: + * 'source' => Source filename or directory which will be renamed + * 'target' => Target filename or directory, the new name of the sourcefile + * 'overwrite' => Shall existing files be overwritten? + * 'randomize' => Shall target files have a random postfix attached? + * + * @param string|array $options Old file or directory to be rewritten + * @return Rename + * @throws Exception\InvalidArgumentException + */ + public function addFile($options) + { + if (is_string($options)) { + $options = array('target' => $options); + } elseif (!is_array($options)) { + throw new Exception\InvalidArgumentException( + 'Invalid options to rename filter provided' + ); + } + + $this->_convertOptions($options); + + return $this; + } + + /** + * Returns only the new filename without moving it + * But existing files will be erased when the overwrite option is true + * + * @param string $value Full path of file to change + * @param bool $source Return internal informations + * @return string The new filename which has been set + * @throws Exception\InvalidArgumentException If the target file already exists. + */ + public function getNewName($value, $source = false) + { + $file = $this->_getFileName($value); + if (!is_array($file)) { + return $file; + } + + if ($file['source'] == $file['target']) { + return $value; + } + + if (!file_exists($file['source'])) { + return $value; + } + + if ($file['overwrite'] && file_exists($file['target'])) { + unlink($file['target']); + } + + if (file_exists($file['target'])) { + throw new Exception\InvalidArgumentException( + sprintf("File '%s' could not be renamed. It already exists.", $value) + ); + } + + if ($source) { + return $file; + } + + return $file['target']; + } + + /** + * Defined by Zend\Filter\Filter + * + * Renames the file $value to the new name set before + * Returns the file $value, removing all but digit characters + * + * @param string|array $value Full path of file to change or $_FILES data array + * @throws Exception\RuntimeException + * @return string|array The new filename which has been set + */ + public function filter($value) + { + if (!is_scalar($value) && !is_array($value)) { + return $value; + } + + // An uploaded file? Retrieve the 'tmp_name' + $isFileUpload = false; + if (is_array($value)) { + if (!isset($value['tmp_name'])) { + return $value; + } + + $isFileUpload = true; + $uploadData = $value; + $value = $value['tmp_name']; + } + + $file = $this->getNewName($value, true); + if (is_string($file)) { + if ($isFileUpload) { + return $uploadData; + } else { + return $file; + } + } + + $result = rename($file['source'], $file['target']); + + if ($result !== true) { + throw new Exception\RuntimeException( + sprintf( + "File '%s' could not be renamed. " . + "An error occurred while processing the file.", + $value + ) + ); + } + + if ($isFileUpload) { + $uploadData['tmp_name'] = $file['target']; + return $uploadData; + } + return $file['target']; + } + + /** + * Internal method for creating the file array + * Supports single and nested arrays + * + * @param array $options + * @return array + */ + protected function _convertOptions($options) + { + $files = array(); + foreach ($options as $key => $value) { + if (is_array($value)) { + $this->_convertOptions($value); + continue; + } + + switch ($key) { + case "source": + $files['source'] = (string) $value; + break; + + case 'target' : + $files['target'] = (string) $value; + break; + + case 'overwrite' : + $files['overwrite'] = (bool) $value; + break; + + case 'randomize' : + $files['randomize'] = (bool) $value; + break; + + default: + break; + } + } + + if (empty($files)) { + return $this; + } + + if (empty($files['source'])) { + $files['source'] = '*'; + } + + if (empty($files['target'])) { + $files['target'] = '*'; + } + + if (empty($files['overwrite'])) { + $files['overwrite'] = false; + } + + if (empty($files['randomize'])) { + $files['randomize'] = false; + } + + $found = false; + foreach ($this->files as $key => $value) { + if ($value['source'] == $files['source']) { + $this->files[$key] = $files; + $found = true; + } + } + + if (!$found) { + $count = count($this->files); + $this->files[$count] = $files; + } + + return $this; + } + + /** + * Internal method to resolve the requested source + * and return all other related parameters + * + * @param string $file Filename to get the informations for + * @return array|string + */ + protected function _getFileName($file) + { + $rename = array(); + foreach ($this->files as $value) { + if ($value['source'] == '*') { + if (!isset($rename['source'])) { + $rename = $value; + $rename['source'] = $file; + } + } + + if ($value['source'] == $file) { + $rename = $value; + break; + } + } + + if (!isset($rename['source'])) { + return $file; + } + + if (!isset($rename['target']) || $rename['target'] == '*') { + $rename['target'] = $rename['source']; + } + + if (is_dir($rename['target'])) { + $name = basename($rename['source']); + $last = $rename['target'][strlen($rename['target']) - 1]; + if (($last != '/') && ($last != '\\')) { + $rename['target'] .= DIRECTORY_SEPARATOR; + } + + $rename['target'] .= $name; + } + + if ($rename['randomize']) { + $info = pathinfo($rename['target']); + $newTarget = $info['dirname'] . DIRECTORY_SEPARATOR . + $info['filename'] . uniqid('_'); + if (isset($info['extension'])) { + $newTarget .= '.' . $info['extension']; + } + $rename['target'] = $newTarget; + } + + return $rename; + } +} diff --git a/library/Zend/Filter/File/RenameUpload.php b/library/Zend/Filter/File/RenameUpload.php new file mode 100755 index 0000000000..e0b5d0b5e9 --- /dev/null +++ b/library/Zend/Filter/File/RenameUpload.php @@ -0,0 +1,313 @@ + null, + 'use_upload_name' => false, + 'use_upload_extension' => false, + 'overwrite' => false, + 'randomize' => false, + ); + + /** + * Store already filtered values, so we can filter multiple + * times the same file without being block by move_uploaded_file + * internal checks + * + * @var array + */ + protected $alreadyFiltered = array(); + + /** + * Constructor + * + * @param array|string $targetOrOptions The target file path or an options array + */ + public function __construct($targetOrOptions) + { + if (is_array($targetOrOptions)) { + $this->setOptions($targetOrOptions); + } else { + $this->setTarget($targetOrOptions); + } + } + + /** + * @param string $target Target file path or directory + * @return self + */ + public function setTarget($target) + { + if (!is_string($target)) { + throw new Exception\InvalidArgumentException( + 'Invalid target, must be a string' + ); + } + $this->options['target'] = $target; + return $this; + } + + /** + * @return string Target file path or directory + */ + public function getTarget() + { + return $this->options['target']; + } + + /** + * @param bool $flag When true, this filter will use the $_FILES['name'] + * as the target filename. + * Otherwise, it uses the default 'target' rules. + * @return self + */ + public function setUseUploadName($flag = true) + { + $this->options['use_upload_name'] = (bool) $flag; + return $this; + } + + /** + * @return bool + */ + public function getUseUploadName() + { + return $this->options['use_upload_name']; + } + + /** + * @param bool $flag When true, this filter will use the original file + * extension for the target filename + * @return self + */ + public function setUseUploadExtension($flag = true) + { + $this->options['use_upload_extension'] = (bool) $flag; + return $this; + } + + /** + * @return bool + */ + public function getUseUploadExtension() + { + return $this->options['use_upload_extension']; + } + + /** + * @param bool $flag Shall existing files be overwritten? + * @return self + */ + public function setOverwrite($flag = true) + { + $this->options['overwrite'] = (bool) $flag; + return $this; + } + + /** + * @return bool + */ + public function getOverwrite() + { + return $this->options['overwrite']; + } + + /** + * @param bool $flag Shall target files have a random postfix attached? + * @return self + */ + public function setRandomize($flag = true) + { + $this->options['randomize'] = (bool) $flag; + return $this; + } + + /** + * @return bool + */ + public function getRandomize() + { + return $this->options['randomize']; + } + + /** + * Defined by Zend\Filter\Filter + * + * Renames the file $value to the new name set before + * Returns the file $value, removing all but digit characters + * + * @param string|array $value Full path of file to change or $_FILES data array + * @throws Exception\RuntimeException + * @return string|array The new filename which has been set, or false when there were errors + */ + public function filter($value) + { + if (!is_scalar($value) && !is_array($value)) { + return $value; + } + + // An uploaded file? Retrieve the 'tmp_name' + $isFileUpload = false; + if (is_array($value)) { + if (!isset($value['tmp_name'])) { + return $value; + } + + $isFileUpload = true; + $uploadData = $value; + $sourceFile = $value['tmp_name']; + } else { + $uploadData = array( + 'tmp_name' => $value, + 'name' => $value, + ); + $sourceFile = $value; + } + + if (isset($this->alreadyFiltered[$sourceFile])) { + return $this->alreadyFiltered[$sourceFile]; + } + + $targetFile = $this->getFinalTarget($uploadData); + if (!file_exists($sourceFile) || $sourceFile == $targetFile) { + return $value; + } + + $this->checkFileExists($targetFile); + $this->moveUploadedFile($sourceFile, $targetFile); + + $return = $targetFile; + if ($isFileUpload) { + $return = $uploadData; + $return['tmp_name'] = $targetFile; + } + + $this->alreadyFiltered[$sourceFile] = $return; + + return $return; + } + + /** + * @param string $sourceFile Source file path + * @param string $targetFile Target file path + * @throws Exception\RuntimeException + * @return bool + */ + protected function moveUploadedFile($sourceFile, $targetFile) + { + ErrorHandler::start(); + $result = move_uploaded_file($sourceFile, $targetFile); + $warningException = ErrorHandler::stop(); + if (!$result || null !== $warningException) { + throw new Exception\RuntimeException( + sprintf("File '%s' could not be renamed. An error occurred while processing the file.", $sourceFile), + 0, $warningException + ); + } + + return $result; + } + + /** + * @param string $targetFile Target file path + * @throws Exception\InvalidArgumentException + */ + protected function checkFileExists($targetFile) + { + if (file_exists($targetFile)) { + if ($this->getOverwrite()) { + unlink($targetFile); + } else { + throw new Exception\InvalidArgumentException( + sprintf("File '%s' could not be renamed. It already exists.", $targetFile) + ); + } + } + } + + /** + * @param array $uploadData $_FILES array + * @return string + */ + protected function getFinalTarget($uploadData) + { + $source = $uploadData['tmp_name']; + $target = $this->getTarget(); + if (!isset($target) || $target == '*') { + $target = $source; + } + + // Get the target directory + if (is_dir($target)) { + $targetDir = $target; + $last = $target[strlen($target) - 1]; + if (($last != '/') && ($last != '\\')) { + $targetDir .= DIRECTORY_SEPARATOR; + } + } else { + $info = pathinfo($target); + $targetDir = $info['dirname'] . DIRECTORY_SEPARATOR; + } + + // Get the target filename + if ($this->getUseUploadName()) { + $targetFile = basename($uploadData['name']); + } elseif (!is_dir($target)) { + $targetFile = basename($target); + if ($this->getUseUploadExtension() && !$this->getRandomize()) { + $targetInfo = pathinfo($targetFile); + $sourceinfo = pathinfo($uploadData['name']); + if (isset($sourceinfo['extension'])) { + $targetFile = $targetInfo['filename'] . '.' . $sourceinfo['extension']; + } + } + } else { + $targetFile = basename($source); + } + + if ($this->getRandomize()) { + $targetFile = $this->applyRandomToFilename($uploadData['name'], $targetFile); + } + + return $targetDir . $targetFile; + } + + /** + * @param string $source + * @param string $filename + * @return string + */ + protected function applyRandomToFilename($source, $filename) + { + $info = pathinfo($filename); + $filename = $info['filename'] . uniqid('_'); + + $sourceinfo = pathinfo($source); + + $extension = ''; + if ($this->getUseUploadExtension() === true && isset($sourceinfo['extension'])) { + $extension .= '.' . $sourceinfo['extension']; + } elseif (isset($info['extension'])) { + $extension .= '.' . $info['extension']; + } + + return $filename . $extension; + } +} diff --git a/library/Zend/Filter/File/UpperCase.php b/library/Zend/Filter/File/UpperCase.php new file mode 100755 index 0000000000..22bf09279e --- /dev/null +++ b/library/Zend/Filter/File/UpperCase.php @@ -0,0 +1,70 @@ +filters = new PriorityQueue(); + + if (null !== $options) { + $this->setOptions($options); + } + } + + /** + * @param array|Traversable $options + * @return self + * @throws Exception\InvalidArgumentException + */ + public function setOptions($options) + { + if (!is_array($options) && !$options instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + 'Expected array or Traversable; received "%s"', + (is_object($options) ? get_class($options) : gettype($options)) + )); + } + + foreach ($options as $key => $value) { + switch (strtolower($key)) { + case 'callbacks': + foreach ($value as $spec) { + $callback = isset($spec['callback']) ? $spec['callback'] : false; + $priority = isset($spec['priority']) ? $spec['priority'] : static::DEFAULT_PRIORITY; + if ($callback) { + $this->attach($callback, $priority); + } + } + break; + case 'filters': + foreach ($value as $spec) { + $name = isset($spec['name']) ? $spec['name'] : false; + $options = isset($spec['options']) ? $spec['options'] : array(); + $priority = isset($spec['priority']) ? $spec['priority'] : static::DEFAULT_PRIORITY; + if ($name) { + $this->attachByName($name, $options, $priority); + } + } + break; + default: + // ignore other options + break; + } + } + + return $this; + } + + /** + * Return the count of attached filters + * + * @return int + */ + public function count() + { + return count($this->filters); + } + + /** + * Get plugin manager instance + * + * @return FilterPluginManager + */ + public function getPluginManager() + { + if (!$this->plugins) { + $this->setPluginManager(new FilterPluginManager()); + } + return $this->plugins; + } + + /** + * Set plugin manager instance + * + * @param FilterPluginManager $plugins + * @return self + */ + public function setPluginManager(FilterPluginManager $plugins) + { + $this->plugins = $plugins; + return $this; + } + + /** + * Retrieve a filter plugin by name + * + * @param mixed $name + * @param array $options + * @return FilterInterface + */ + public function plugin($name, array $options = array()) + { + $plugins = $this->getPluginManager(); + return $plugins->get($name, $options); + } + + /** + * Attach a filter to the chain + * + * @param callable|FilterInterface $callback A Filter implementation or valid PHP callback + * @param int $priority Priority at which to enqueue filter; defaults to 1000 (higher executes earlier) + * @throws Exception\InvalidArgumentException + * @return self + */ + public function attach($callback, $priority = self::DEFAULT_PRIORITY) + { + if (!is_callable($callback)) { + if (!$callback instanceof FilterInterface) { + throw new Exception\InvalidArgumentException(sprintf( + 'Expected a valid PHP callback; received "%s"', + (is_object($callback) ? get_class($callback) : gettype($callback)) + )); + } + $callback = array($callback, 'filter'); + } + $this->filters->insert($callback, $priority); + return $this; + } + + /** + * Attach a filter to the chain using a short name + * + * Retrieves the filter from the attached plugin manager, and then calls attach() + * with the retrieved instance. + * + * @param string $name + * @param mixed $options + * @param int $priority Priority at which to enqueue filter; defaults to 1000 (higher executes earlier) + * @return self + */ + public function attachByName($name, $options = array(), $priority = self::DEFAULT_PRIORITY) + { + if (!is_array($options)) { + $options = (array) $options; + } elseif (empty($options)) { + $options = null; + } + $filter = $this->getPluginManager()->get($name, $options); + return $this->attach($filter, $priority); + } + + /** + * Merge the filter chain with the one given in parameter + * + * @param FilterChain $filterChain + * @return self + */ + public function merge(FilterChain $filterChain) + { + foreach ($filterChain->filters->toArray(PriorityQueue::EXTR_BOTH) as $item) { + $this->attach($item['data'], $item['priority']); + } + + return $this; + } + + /** + * Get all the filters + * + * @return PriorityQueue + */ + public function getFilters() + { + return $this->filters; + } + + /** + * Returns $value filtered through each filter in the chain + * + * Filters are run in the order in which they were added to the chain (FIFO) + * + * @param mixed $value + * @return mixed + */ + public function filter($value) + { + $chain = clone $this->filters; + + $valueFiltered = $value; + foreach ($chain as $filter) { + $valueFiltered = call_user_func($filter, $valueFiltered); + } + + return $valueFiltered; + } + + /** + * Clone filters + */ + public function __clone() + { + $this->filters = clone $this->filters; + } + + /** + * Prepare filter chain for serialization + * + * Plugin manager (property 'plugins') cannot + * be serialized. On wakeup the property remains unset + * and next invocation to getPluginManager() sets + * the default plugin manager instance (FilterPluginManager). + */ + public function __sleep() + { + return array('filters'); + } +} diff --git a/library/Zend/Filter/FilterInterface.php b/library/Zend/Filter/FilterInterface.php new file mode 100755 index 0000000000..2b3c163538 --- /dev/null +++ b/library/Zend/Filter/FilterInterface.php @@ -0,0 +1,22 @@ + 'Zend\I18n\Filter\Alnum', + 'alpha' => 'Zend\I18n\Filter\Alpha', + 'basename' => 'Zend\Filter\BaseName', + 'boolean' => 'Zend\Filter\Boolean', + 'callback' => 'Zend\Filter\Callback', + 'compress' => 'Zend\Filter\Compress', + 'compressbz2' => 'Zend\Filter\Compress\Bz2', + 'compressgz' => 'Zend\Filter\Compress\Gz', + 'compresslzf' => 'Zend\Filter\Compress\Lzf', + 'compressrar' => 'Zend\Filter\Compress\Rar', + 'compresssnappy' => 'Zend\Filter\Compress\Snappy', + 'compresstar' => 'Zend\Filter\Compress\Tar', + 'compresszip' => 'Zend\Filter\Compress\Zip', + 'datetimeformatter' => 'Zend\Filter\DateTimeFormatter', + 'decompress' => 'Zend\Filter\Decompress', + 'decrypt' => 'Zend\Filter\Decrypt', + 'digits' => 'Zend\Filter\Digits', + 'dir' => 'Zend\Filter\Dir', + 'encrypt' => 'Zend\Filter\Encrypt', + 'encryptblockcipher' => 'Zend\Filter\Encrypt\BlockCipher', + 'encryptopenssl' => 'Zend\Filter\Encrypt\Openssl', + 'filedecrypt' => 'Zend\Filter\File\Decrypt', + 'fileencrypt' => 'Zend\Filter\File\Encrypt', + 'filelowercase' => 'Zend\Filter\File\LowerCase', + 'filerename' => 'Zend\Filter\File\Rename', + 'filerenameupload' => 'Zend\Filter\File\RenameUpload', + 'fileuppercase' => 'Zend\Filter\File\UpperCase', + 'htmlentities' => 'Zend\Filter\HtmlEntities', + 'inflector' => 'Zend\Filter\Inflector', + 'int' => 'Zend\Filter\Int', + 'null' => 'Zend\Filter\Null', + 'numberformat' => 'Zend\I18n\Filter\NumberFormat', + 'numberparse' => 'Zend\I18n\Filter\NumberParse', + 'pregreplace' => 'Zend\Filter\PregReplace', + 'realpath' => 'Zend\Filter\RealPath', + 'stringtolower' => 'Zend\Filter\StringToLower', + 'stringtoupper' => 'Zend\Filter\StringToUpper', + 'stringtrim' => 'Zend\Filter\StringTrim', + 'stripnewlines' => 'Zend\Filter\StripNewlines', + 'striptags' => 'Zend\Filter\StripTags', + 'urinormalize' => 'Zend\Filter\UriNormalize', + 'wordcamelcasetodash' => 'Zend\Filter\Word\CamelCaseToDash', + 'wordcamelcasetoseparator' => 'Zend\Filter\Word\CamelCaseToSeparator', + 'wordcamelcasetounderscore' => 'Zend\Filter\Word\CamelCaseToUnderscore', + 'worddashtocamelcase' => 'Zend\Filter\Word\DashToCamelCase', + 'worddashtoseparator' => 'Zend\Filter\Word\DashToSeparator', + 'worddashtounderscore' => 'Zend\Filter\Word\DashToUnderscore', + 'wordseparatortocamelcase' => 'Zend\Filter\Word\SeparatorToCamelCase', + 'wordseparatortodash' => 'Zend\Filter\Word\SeparatorToDash', + 'wordseparatortoseparator' => 'Zend\Filter\Word\SeparatorToSeparator', + 'wordunderscoretocamelcase' => 'Zend\Filter\Word\UnderscoreToCamelCase', + 'wordunderscoretodash' => 'Zend\Filter\Word\UnderscoreToDash', + 'wordunderscoretoseparator' => 'Zend\Filter\Word\UnderscoreToSeparator', + ); + + /** + * Whether or not to share by default; default to false + * + * @var bool + */ + protected $shareByDefault = false; + + /** + * Validate the plugin + * + * Checks that the filter loaded is either a valid callback or an instance + * of FilterInterface. + * + * @param mixed $plugin + * @return void + * @throws Exception\RuntimeException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof FilterInterface) { + // we're okay + return; + } + if (is_callable($plugin)) { + // also okay + return; + } + + throw new Exception\RuntimeException(sprintf( + 'Plugin of type %s is invalid; must implement %s\FilterInterface or be callable', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/library/Zend/Filter/HtmlEntities.php b/library/Zend/Filter/HtmlEntities.php new file mode 100755 index 0000000000..2abff010b1 --- /dev/null +++ b/library/Zend/Filter/HtmlEntities.php @@ -0,0 +1,203 @@ +setQuoteStyle($options['quotestyle']); + $this->setEncoding($options['encoding']); + $this->setDoubleQuote($options['doublequote']); + } + + /** + * Returns the quoteStyle option + * + * @return int + */ + public function getQuoteStyle() + { + return $this->quoteStyle; + } + + /** + * Sets the quoteStyle option + * + * @param int $quoteStyle + * @return self Provides a fluent interface + */ + public function setQuoteStyle($quoteStyle) + { + $this->quoteStyle = $quoteStyle; + return $this; + } + + + /** + * Get encoding + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Set encoding + * + * @param string $value + * @return self + */ + public function setEncoding($value) + { + $this->encoding = (string) $value; + return $this; + } + + /** + * Returns the charSet option + * + * Proxies to {@link getEncoding()} + * + * @return string + */ + public function getCharSet() + { + return $this->getEncoding(); + } + + /** + * Sets the charSet option + * + * Proxies to {@link setEncoding()} + * + * @param string $charSet + * @return self Provides a fluent interface + */ + public function setCharSet($charSet) + { + return $this->setEncoding($charSet); + } + + /** + * Returns the doubleQuote option + * + * @return bool + */ + public function getDoubleQuote() + { + return $this->doubleQuote; + } + + /** + * Sets the doubleQuote option + * + * @param bool $doubleQuote + * @return self Provides a fluent interface + */ + public function setDoubleQuote($doubleQuote) + { + $this->doubleQuote = (bool) $doubleQuote; + return $this; + } + + /** + * Defined by Zend\Filter\FilterInterface + * + * Returns the string $value, converting characters to their corresponding HTML entity + * equivalents where they exist + * + * If the value provided is non-scalar, the value will remain unfiltered + * + * @param string $value + * @return string|mixed + * @throws Exception\DomainException on encoding mismatches + */ + public function filter($value) + { + if (!is_scalar($value)) { + return $value; + } + $value = (string) $value; + + $filtered = htmlentities($value, $this->getQuoteStyle(), $this->getEncoding(), $this->getDoubleQuote()); + if (strlen($value) && !strlen($filtered)) { + if (!function_exists('iconv')) { + throw new Exception\DomainException('Encoding mismatch has resulted in htmlentities errors'); + } + $enc = $this->getEncoding(); + $value = iconv('', $this->getEncoding() . '//IGNORE', $value); + $filtered = htmlentities($value, $this->getQuoteStyle(), $enc, $this->getDoubleQuote()); + if (!strlen($filtered)) { + throw new Exception\DomainException('Encoding mismatch has resulted in htmlentities errors'); + } + } + return $filtered; + } +} diff --git a/library/Zend/Filter/Inflector.php b/library/Zend/Filter/Inflector.php new file mode 100755 index 0000000000..4120a68f2e --- /dev/null +++ b/library/Zend/Filter/Inflector.php @@ -0,0 +1,472 @@ +setOptions($options); + } + + /** + * Retrieve plugin manager + * + * @return FilterPluginManager + */ + public function getPluginManager() + { + if (!$this->pluginManager instanceof FilterPluginManager) { + $this->setPluginManager(new FilterPluginManager()); + } + + return $this->pluginManager; + } + + /** + * Set plugin manager + * + * @param FilterPluginManager $manager + * @return self + */ + public function setPluginManager(FilterPluginManager $manager) + { + $this->pluginManager = $manager; + return $this; + } + + /** + * Set options + * + * @param array|Traversable $options + * @return self + */ + public function setOptions($options) + { + if ($options instanceof Traversable) { + $options = ArrayUtils::iteratorToArray($options); + } + + // Set plugin manager + if (array_key_exists('pluginManager', $options)) { + if (is_scalar($options['pluginManager']) && class_exists($options['pluginManager'])) { + $options['pluginManager'] = new $options['pluginManager']; + } + $this->setPluginManager($options['pluginManager']); + } + + if (array_key_exists('throwTargetExceptionsOn', $options)) { + $this->setThrowTargetExceptionsOn($options['throwTargetExceptionsOn']); + } + + if (array_key_exists('targetReplacementIdentifier', $options)) { + $this->setTargetReplacementIdentifier($options['targetReplacementIdentifier']); + } + + if (array_key_exists('target', $options)) { + $this->setTarget($options['target']); + } + + if (array_key_exists('rules', $options)) { + $this->addRules($options['rules']); + } + + return $this; + } + + /** + * Set Whether or not the inflector should throw an exception when a replacement + * identifier is still found within an inflected target. + * + * @param bool $throwTargetExceptionsOn + * @return self + */ + public function setThrowTargetExceptionsOn($throwTargetExceptionsOn) + { + $this->throwTargetExceptionsOn = ($throwTargetExceptionsOn == true) ? true : false; + return $this; + } + + /** + * Will exceptions be thrown? + * + * @return bool + */ + public function isThrowTargetExceptionsOn() + { + return $this->throwTargetExceptionsOn; + } + + /** + * Set the Target Replacement Identifier, by default ':' + * + * @param string $targetReplacementIdentifier + * @return self + */ + public function setTargetReplacementIdentifier($targetReplacementIdentifier) + { + if ($targetReplacementIdentifier) { + $this->targetReplacementIdentifier = (string) $targetReplacementIdentifier; + } + + return $this; + } + + /** + * Get Target Replacement Identifier + * + * @return string + */ + public function getTargetReplacementIdentifier() + { + return $this->targetReplacementIdentifier; + } + + /** + * Set a Target + * ex: 'scripts/:controller/:action.:suffix' + * + * @param string $target + * @return self + */ + public function setTarget($target) + { + $this->target = (string) $target; + return $this; + } + + /** + * Retrieve target + * + * @return string + */ + public function getTarget() + { + return $this->target; + } + + /** + * Set Target Reference + * + * @param string $target + * @return self + */ + public function setTargetReference(&$target) + { + $this->target =& $target; + return $this; + } + + /** + * Is the same as calling addRules() with the exception that it + * clears the rules before adding them. + * + * @param array $rules + * @return self + */ + public function setRules(array $rules) + { + $this->clearRules(); + $this->addRules($rules); + return $this; + } + + /** + * Multi-call to setting filter rules. + * + * If prefixed with a ":" (colon), a filter rule will be added. If not + * prefixed, a static replacement will be added. + * + * ex: + * array( + * ':controller' => array('CamelCaseToUnderscore', 'StringToLower'), + * ':action' => array('CamelCaseToUnderscore', 'StringToLower'), + * 'suffix' => 'phtml' + * ); + * + * @param array $rules + * @return self + */ + public function addRules(array $rules) + { + $keys = array_keys($rules); + foreach ($keys as $spec) { + if ($spec[0] == ':') { + $this->addFilterRule($spec, $rules[$spec]); + } else { + $this->setStaticRule($spec, $rules[$spec]); + } + } + + return $this; + } + + /** + * Get rules + * + * By default, returns all rules. If a $spec is provided, will return those + * rules if found, false otherwise. + * + * @param string $spec + * @return array|false + */ + public function getRules($spec = null) + { + if (null !== $spec) { + $spec = $this->_normalizeSpec($spec); + if (isset($this->rules[$spec])) { + return $this->rules[$spec]; + } + return false; + } + + return $this->rules; + } + + /** + * Returns a rule set by setFilterRule(), a numeric index must be provided + * + * @param string $spec + * @param int $index + * @return FilterInterface|false + */ + public function getRule($spec, $index) + { + $spec = $this->_normalizeSpec($spec); + if (isset($this->rules[$spec]) && is_array($this->rules[$spec])) { + if (isset($this->rules[$spec][$index])) { + return $this->rules[$spec][$index]; + } + } + return false; + } + + /** + * Clears the rules currently in the inflector + * + * @return self + */ + public function clearRules() + { + $this->rules = array(); + return $this; + } + + /** + * Set a filtering rule for a spec. $ruleSet can be a string, Filter object + * or an array of strings or filter objects. + * + * @param string $spec + * @param array|string|\Zend\Filter\FilterInterface $ruleSet + * @return self + */ + public function setFilterRule($spec, $ruleSet) + { + $spec = $this->_normalizeSpec($spec); + $this->rules[$spec] = array(); + return $this->addFilterRule($spec, $ruleSet); + } + + /** + * Add a filter rule for a spec + * + * @param mixed $spec + * @param mixed $ruleSet + * @return self + */ + public function addFilterRule($spec, $ruleSet) + { + $spec = $this->_normalizeSpec($spec); + if (!isset($this->rules[$spec])) { + $this->rules[$spec] = array(); + } + + if (!is_array($ruleSet)) { + $ruleSet = array($ruleSet); + } + + if (is_string($this->rules[$spec])) { + $temp = $this->rules[$spec]; + $this->rules[$spec] = array(); + $this->rules[$spec][] = $temp; + } + + foreach ($ruleSet as $rule) { + $this->rules[$spec][] = $this->_getRule($rule); + } + + return $this; + } + + /** + * Set a static rule for a spec. This is a single string value + * + * @param string $name + * @param string $value + * @return self + */ + public function setStaticRule($name, $value) + { + $name = $this->_normalizeSpec($name); + $this->rules[$name] = (string) $value; + return $this; + } + + /** + * Set Static Rule Reference. + * + * This allows a consuming class to pass a property or variable + * in to be referenced when its time to build the output string from the + * target. + * + * @param string $name + * @param mixed $reference + * @return self + */ + public function setStaticRuleReference($name, &$reference) + { + $name = $this->_normalizeSpec($name); + $this->rules[$name] =& $reference; + return $this; + } + + /** + * Inflect + * + * @param string|array $source + * @throws Exception\RuntimeException + * @return string + */ + public function filter($source) + { + // clean source + foreach ((array) $source as $sourceName => $sourceValue) { + $source[ltrim($sourceName, ':')] = $sourceValue; + } + + $pregQuotedTargetReplacementIdentifier = preg_quote($this->targetReplacementIdentifier, '#'); + $processedParts = array(); + + foreach ($this->rules as $ruleName => $ruleValue) { + if (isset($source[$ruleName])) { + if (is_string($ruleValue)) { + // overriding the set rule + $processedParts['#' . $pregQuotedTargetReplacementIdentifier . $ruleName . '#'] = str_replace('\\', '\\\\', $source[$ruleName]); + } elseif (is_array($ruleValue)) { + $processedPart = $source[$ruleName]; + foreach ($ruleValue as $ruleFilter) { + $processedPart = $ruleFilter($processedPart); + } + $processedParts['#' . $pregQuotedTargetReplacementIdentifier . $ruleName . '#'] = str_replace('\\', '\\\\', $processedPart); + } + } elseif (is_string($ruleValue)) { + $processedParts['#' . $pregQuotedTargetReplacementIdentifier . $ruleName . '#'] = str_replace('\\', '\\\\', $ruleValue); + } + } + + // all of the values of processedParts would have been str_replace('\\', '\\\\', ..)'d to disable preg_replace backreferences + $inflectedTarget = preg_replace(array_keys($processedParts), array_values($processedParts), $this->target); + + if ($this->throwTargetExceptionsOn && (preg_match('#(?=' . $pregQuotedTargetReplacementIdentifier.'[A-Za-z]{1})#', $inflectedTarget) == true)) { + throw new Exception\RuntimeException('A replacement identifier ' . $this->targetReplacementIdentifier . ' was found inside the inflected target, perhaps a rule was not satisfied with a target source? Unsatisfied inflected target: ' . $inflectedTarget); + } + + return $inflectedTarget; + } + + /** + * Normalize spec string + * + * @param string $spec + * @return string + */ + protected function _normalizeSpec($spec) + { + return ltrim((string) $spec, ':&'); + } + + /** + * Resolve named filters and convert them to filter objects. + * + * @param string $rule + * @return FilterInterface + */ + protected function _getRule($rule) + { + if ($rule instanceof FilterInterface) { + return $rule; + } + + $rule = (string) $rule; + return $this->getPluginManager()->get($rule); + } +} diff --git a/library/Zend/Filter/Int.php b/library/Zend/Filter/Int.php new file mode 100755 index 0000000000..0f2b80dba4 --- /dev/null +++ b/library/Zend/Filter/Int.php @@ -0,0 +1,33 @@ + 'boolean', + self::TYPE_INTEGER => 'integer', + self::TYPE_EMPTY_ARRAY => 'array', + self::TYPE_STRING => 'string', + self::TYPE_ZERO_STRING => 'zero', + self::TYPE_FLOAT => 'float', + self::TYPE_ALL => 'all', + ); + + /** + * @var array + */ + protected $options = array( + 'type' => self::TYPE_ALL, + ); + + /** + * Constructor + * + * @param string|array|Traversable $typeOrOptions OPTIONAL + */ + public function __construct($typeOrOptions = null) + { + if ($typeOrOptions !== null) { + if ($typeOrOptions instanceof Traversable) { + $typeOrOptions = iterator_to_array($typeOrOptions); + } + + if (is_array($typeOrOptions)) { + if (isset($typeOrOptions['type'])) { + $this->setOptions($typeOrOptions); + } else { + $this->setType($typeOrOptions); + } + } else { + $this->setType($typeOrOptions); + } + } + } + + /** + * Set boolean types + * + * @param int|array $type + * @throws Exception\InvalidArgumentException + * @return self + */ + public function setType($type = null) + { + if (is_array($type)) { + $detected = 0; + foreach ($type as $value) { + if (is_int($value)) { + $detected += $value; + } elseif (in_array($value, $this->constants)) { + $detected += array_search($value, $this->constants); + } + } + + $type = $detected; + } elseif (is_string($type) && in_array($type, $this->constants)) { + $type = array_search($type, $this->constants); + } + + if (!is_int($type) || ($type < 0) || ($type > self::TYPE_ALL)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Unknown type value "%s" (%s)', + $type, + gettype($type) + )); + } + + $this->options['type'] = $type; + return $this; + } + + /** + * Returns defined boolean types + * + * @return int + */ + public function getType() + { + return $this->options['type']; + } + + /** + * Defined by Zend\Filter\FilterInterface + * + * Returns null representation of $value, if value is empty and matches + * types that should be considered null. + * + * @param string $value + * @return string + */ + public function filter($value) + { + $type = $this->getType(); + + // FLOAT (0.0) + if ($type >= self::TYPE_FLOAT) { + $type -= self::TYPE_FLOAT; + if (is_float($value) && ($value == 0.0)) { + return null; + } + } + + // STRING ZERO ('0') + if ($type >= self::TYPE_ZERO_STRING) { + $type -= self::TYPE_ZERO_STRING; + if (is_string($value) && ($value == '0')) { + return null; + } + } + + // STRING ('') + if ($type >= self::TYPE_STRING) { + $type -= self::TYPE_STRING; + if (is_string($value) && ($value == '')) { + return null; + } + } + + // EMPTY_ARRAY (array()) + if ($type >= self::TYPE_EMPTY_ARRAY) { + $type -= self::TYPE_EMPTY_ARRAY; + if (is_array($value) && ($value == array())) { + return null; + } + } + + // INTEGER (0) + if ($type >= self::TYPE_INTEGER) { + $type -= self::TYPE_INTEGER; + if (is_int($value) && ($value == 0)) { + return null; + } + } + + // BOOLEAN (false) + if ($type >= self::TYPE_BOOLEAN) { + $type -= self::TYPE_BOOLEAN; + if (is_bool($value) && ($value == false)) { + return null; + } + } + + return $value; + } +} diff --git a/library/Zend/Filter/PregReplace.php b/library/Zend/Filter/PregReplace.php new file mode 100755 index 0000000000..9a7e3f3adc --- /dev/null +++ b/library/Zend/Filter/PregReplace.php @@ -0,0 +1,166 @@ + null, + 'replacement' => '', + ); + + /** + * Constructor + * Supported options are + * 'pattern' => matching pattern + * 'replacement' => replace with this + * + * @param array|Traversable|string|null $options + */ + public function __construct($options = null) + { + if ($options instanceof Traversable) { + $options = iterator_to_array($options); + } + + if (!is_array($options) + || (!isset($options['pattern']) && !isset($options['replacement'])) + ) { + $args = func_get_args(); + if (isset($args[0])) { + $this->setPattern($args[0]); + } + if (isset($args[1])) { + $this->setReplacement($args[1]); + } + } else { + $this->setOptions($options); + } + } + + /** + * Set the regex pattern to search for + * @see preg_replace() + * + * @param string|array $pattern - same as the first argument of preg_replace + * @return self + * @throws Exception\InvalidArgumentException + */ + public function setPattern($pattern) + { + if (!is_array($pattern) && !is_string($pattern)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects pattern to be array or string; received "%s"', + __METHOD__, + (is_object($pattern) ? get_class($pattern) : gettype($pattern)) + )); + } + + if (is_array($pattern)) { + foreach ($pattern as $p) { + $this->validatePattern($p); + } + } + + if (is_string($pattern)) { + $this->validatePattern($pattern); + } + + $this->options['pattern'] = $pattern; + return $this; + } + + /** + * Get currently set match pattern + * + * @return string|array + */ + public function getPattern() + { + return $this->options['pattern']; + } + + /** + * Set the replacement array/string + * @see preg_replace() + * + * @param array|string $replacement - same as the second argument of preg_replace + * @return self + * @throws Exception\InvalidArgumentException + */ + public function setReplacement($replacement) + { + if (!is_array($replacement) && !is_string($replacement)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects replacement to be array or string; received "%s"', + __METHOD__, + (is_object($replacement) ? get_class($replacement) : gettype($replacement)) + )); + } + $this->options['replacement'] = $replacement; + return $this; + } + + /** + * Get currently set replacement value + * + * @return string|array + */ + public function getReplacement() + { + return $this->options['replacement']; + } + + /** + * Perform regexp replacement as filter + * + * @param mixed $value + * @return mixed + * @throws Exception\RuntimeException + */ + public function filter($value) + { + if (!is_scalar($value) && !is_array($value)) { + return $value; + } + + if ($this->options['pattern'] === null) { + throw new Exception\RuntimeException(sprintf( + 'Filter %s does not have a valid pattern set', + get_class($this) + )); + } + + return preg_replace($this->options['pattern'], $this->options['replacement'], $value); + } + + /** + * Validate a pattern and ensure it does not contain the "e" modifier + * + * @param string $pattern + * @return bool + * @throws Exception\InvalidArgumentException + */ + protected function validatePattern($pattern) + { + if (!preg_match('/(?[imsxeADSUXJu]+)$/', $pattern, $matches)) { + return true; + } + + if (false !== strstr($matches['modifier'], 'e')) { + throw new Exception\InvalidArgumentException(sprintf( + 'Pattern for a PregReplace filter may not contain the "e" pattern modifier; received "%s"', + $pattern + )); + } + } +} diff --git a/library/Zend/Filter/README.md b/library/Zend/Filter/README.md new file mode 100755 index 0000000000..2aeb60bf21 --- /dev/null +++ b/library/Zend/Filter/README.md @@ -0,0 +1,15 @@ +Filter Component from ZF2 +========================= + +This is the Filter component for ZF2. + +- File issues at https://github.com/zendframework/zf2/issues +- Create pull requests against https://github.com/zendframework/zf2 +- Documentation is at http://framework.zend.com/docs + +LICENSE +------- + +The files in this archive are released under the [Zend Framework +license](http://framework.zend.com/license), which is a 3-clause BSD license. + diff --git a/library/Zend/Filter/RealPath.php b/library/Zend/Filter/RealPath.php new file mode 100755 index 0000000000..00633d5c9c --- /dev/null +++ b/library/Zend/Filter/RealPath.php @@ -0,0 +1,122 @@ + true + ); + + /** + * Class constructor + * + * @param bool|Traversable $existsOrOptions Options to set + */ + public function __construct($existsOrOptions = true) + { + if ($existsOrOptions !== null) { + if (!static::isOptions($existsOrOptions)) { + $this->setExists($existsOrOptions); + } else { + $this->setOptions($existsOrOptions); + } + } + } + + /** + * Sets if the path has to exist + * TRUE when the path must exist + * FALSE when not existing paths can be given + * + * @param bool $flag Path must exist + * @return self + */ + public function setExists($flag = true) + { + $this->options['exists'] = (bool) $flag; + return $this; + } + + /** + * Returns true if the filtered path must exist + * + * @return bool + */ + public function getExists() + { + return $this->options['exists']; + } + + /** + * Defined by Zend\Filter\FilterInterface + * + * Returns realpath($value) + * + * If the value provided is non-scalar, the value will remain unfiltered + * + * @param string $value + * @return string|mixed + */ + public function filter($value) + { + if (!is_string($value)) { + return $value; + } + $path = (string) $value; + + if ($this->options['exists']) { + return realpath($path); + } + + ErrorHandler::start(); + $realpath = realpath($path); + ErrorHandler::stop(); + if ($realpath) { + return $realpath; + } + + $drive = ''; + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + $path = preg_replace('/[\\\\\/]/', DIRECTORY_SEPARATOR, $path); + if (preg_match('/([a-zA-Z]\:)(.*)/', $path, $matches)) { + list(, $drive, $path) = $matches; + } else { + $cwd = getcwd(); + $drive = substr($cwd, 0, 2); + if (substr($path, 0, 1) != DIRECTORY_SEPARATOR) { + $path = substr($cwd, 3) . DIRECTORY_SEPARATOR . $path; + } + } + } elseif (substr($path, 0, 1) != DIRECTORY_SEPARATOR) { + $path = getcwd() . DIRECTORY_SEPARATOR . $path; + } + + $stack = array(); + $parts = explode(DIRECTORY_SEPARATOR, $path); + foreach ($parts as $dir) { + if (strlen($dir) && $dir !== '.') { + if ($dir == '..') { + array_pop($stack); + } else { + array_push($stack, $dir); + } + } + } + + return $drive . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $stack); + } +} diff --git a/library/Zend/Filter/StaticFilter.php b/library/Zend/Filter/StaticFilter.php new file mode 100755 index 0000000000..2847137b64 --- /dev/null +++ b/library/Zend/Filter/StaticFilter.php @@ -0,0 +1,70 @@ +setShareByDefault(false); + } + static::$plugins = $manager; + } + + /** + * Get plugin manager for loading filter classes + * + * @return FilterPluginManager + */ + public static function getPluginManager() + { + if (null === static::$plugins) { + static::setPluginManager(new FilterPluginManager()); + } + return static::$plugins; + } + + /** + * Returns a value filtered through a specified filter class, without requiring separate + * instantiation of the filter object. + * + * The first argument of this method is a data input value, that you would have filtered. + * The second argument is a string, which corresponds to the basename of the filter class, + * relative to the Zend\Filter namespace. This method automatically loads the class, + * creates an instance, and applies the filter() method to the data input. You can also pass + * an array of constructor arguments, if they are needed for the filter class. + * + * @param mixed $value + * @param string $classBaseName + * @param array $args OPTIONAL + * @return mixed + * @throws Exception\ExceptionInterface + */ + public static function execute($value, $classBaseName, array $args = array()) + { + $plugins = static::getPluginManager(); + + $filter = $plugins->get($classBaseName, $args); + return $filter->filter($value); + } +} diff --git a/library/Zend/Filter/StringToLower.php b/library/Zend/Filter/StringToLower.php new file mode 100755 index 0000000000..3d6f66a462 --- /dev/null +++ b/library/Zend/Filter/StringToLower.php @@ -0,0 +1,62 @@ + null, + ); + + /** + * Constructor + * + * @param string|array|Traversable $encodingOrOptions OPTIONAL + */ + public function __construct($encodingOrOptions = null) + { + if ($encodingOrOptions !== null) { + if (!static::isOptions($encodingOrOptions)) { + $this->setEncoding($encodingOrOptions); + } else { + $this->setOptions($encodingOrOptions); + } + } + } + + /** + * Defined by Zend\Filter\FilterInterface + * + * Returns the string $value, converting characters to lowercase as necessary + * + * If the value provided is non-scalar, the value will remain unfiltered + * + * @param string $value + * @return string|mixed + */ + public function filter($value) + { + if (!is_scalar($value)) { + return $value; + } + $value = (string) $value; + + if ($this->options['encoding'] !== null) { + return mb_strtolower($value, $this->options['encoding']); + } + + return strtolower($value); + } +} diff --git a/library/Zend/Filter/StringToUpper.php b/library/Zend/Filter/StringToUpper.php new file mode 100755 index 0000000000..7f4c78e129 --- /dev/null +++ b/library/Zend/Filter/StringToUpper.php @@ -0,0 +1,62 @@ + null, + ); + + /** + * Constructor + * + * @param string|array|Traversable $encodingOrOptions OPTIONAL + */ + public function __construct($encodingOrOptions = null) + { + if ($encodingOrOptions !== null) { + if (!static::isOptions($encodingOrOptions)) { + $this->setEncoding($encodingOrOptions); + } else { + $this->setOptions($encodingOrOptions); + } + } + } + + /** + * Defined by Zend\Filter\FilterInterface + * + * Returns the string $value, converting characters to uppercase as necessary + * + * If the value provided is non-scalar, the value will remain unfiltered + * + * @param string $value + * @return string|mixed + */ + public function filter($value) + { + if (!is_scalar($value)) { + return $value; + } + $value = (string) $value; + + if ($this->options['encoding'] !== null) { + return mb_strtoupper($value, $this->options['encoding']); + } + + return strtoupper($value); + } +} diff --git a/library/Zend/Filter/StringTrim.php b/library/Zend/Filter/StringTrim.php new file mode 100755 index 0000000000..2eae7fa36c --- /dev/null +++ b/library/Zend/Filter/StringTrim.php @@ -0,0 +1,110 @@ + null, + ); + + /** + * Sets filter options + * + * @param string|array|Traversable $charlistOrOptions + */ + public function __construct($charlistOrOptions = null) + { + if ($charlistOrOptions !== null) { + if (!is_array($charlistOrOptions) + && !$charlistOrOptions instanceof Traversable + ) { + $this->setCharList($charlistOrOptions); + } else { + $this->setOptions($charlistOrOptions); + } + } + } + + /** + * Sets the charList option + * + * @param string $charList + * @return self Provides a fluent interface + */ + public function setCharList($charList) + { + if (! strlen($charList)) { + $charList = null; + } + + $this->options['charlist'] = $charList; + + return $this; + } + + /** + * Returns the charList option + * + * @return string|null + */ + public function getCharList() + { + return $this->options['charlist']; + } + + /** + * Defined by Zend\Filter\FilterInterface + * + * Returns the string $value with characters stripped from the beginning and end + * + * @param string $value + * @return string + */ + public function filter($value) + { + if (!is_string($value)) { + return $value; + } + $value = (string) $value; + + if (null === $this->options['charlist']) { + return $this->unicodeTrim($value); + } + + return $this->unicodeTrim($value, $this->options['charlist']); + } + + /** + * Unicode aware trim method + * Fixes a PHP problem + * + * @param string $value + * @param string $charlist + * @return string + */ + protected function unicodeTrim($value, $charlist = '\\\\s') + { + $chars = preg_replace( + array('/[\^\-\]\\\]/S', '/\\\{4}/S', '/\//'), + array('\\\\\\0', '\\', '\/'), + $charlist + ); + + $pattern = '/^[' . $chars . ']+|[' . $chars . ']+$/usSD'; + + return preg_replace($pattern, '', $value); + } +} diff --git a/library/Zend/Filter/StripNewlines.php b/library/Zend/Filter/StripNewlines.php new file mode 100755 index 0000000000..481facaa88 --- /dev/null +++ b/library/Zend/Filter/StripNewlines.php @@ -0,0 +1,29 @@ + Tags which are allowed + * 'allowAttribs' => Attributes which are allowed + * 'allowComments' => Are comments allowed ? + * + * @param string|array|Traversable $options + */ + public function __construct($options = null) + { + if ($options instanceof Traversable) { + $options = ArrayUtils::iteratorToArray($options); + } + if ((!is_array($options)) || (is_array($options) && !array_key_exists('allowTags', $options) && + !array_key_exists('allowAttribs', $options) && !array_key_exists('allowComments', $options))) { + $options = func_get_args(); + $temp['allowTags'] = array_shift($options); + if (!empty($options)) { + $temp['allowAttribs'] = array_shift($options); + } + + if (!empty($options)) { + $temp['allowComments'] = array_shift($options); + } + + $options = $temp; + } + + if (array_key_exists('allowTags', $options)) { + $this->setTagsAllowed($options['allowTags']); + } + + if (array_key_exists('allowAttribs', $options)) { + $this->setAttributesAllowed($options['allowAttribs']); + } + } + + /** + * Returns the tagsAllowed option + * + * @return array + */ + public function getTagsAllowed() + { + return $this->tagsAllowed; + } + + /** + * Sets the tagsAllowed option + * + * @param array|string $tagsAllowed + * @return self Provides a fluent interface + */ + public function setTagsAllowed($tagsAllowed) + { + if (!is_array($tagsAllowed)) { + $tagsAllowed = array($tagsAllowed); + } + + foreach ($tagsAllowed as $index => $element) { + // If the tag was provided without attributes + if (is_int($index) && is_string($element)) { + // Canonicalize the tag name + $tagName = strtolower($element); + // Store the tag as allowed with no attributes + $this->tagsAllowed[$tagName] = array(); + } + // Otherwise, if a tag was provided with attributes + elseif (is_string($index) && (is_array($element) || is_string($element))) { + // Canonicalize the tag name + $tagName = strtolower($index); + // Canonicalize the attributes + if (is_string($element)) { + $element = array($element); + } + // Store the tag as allowed with the provided attributes + $this->tagsAllowed[$tagName] = array(); + foreach ($element as $attribute) { + if (is_string($attribute)) { + // Canonicalize the attribute name + $attributeName = strtolower($attribute); + $this->tagsAllowed[$tagName][$attributeName] = null; + } + } + } + } + + return $this; + } + + /** + * Returns the attributesAllowed option + * + * @return array + */ + public function getAttributesAllowed() + { + return $this->attributesAllowed; + } + + /** + * Sets the attributesAllowed option + * + * @param array|string $attributesAllowed + * @return self Provides a fluent interface + */ + public function setAttributesAllowed($attributesAllowed) + { + if (!is_array($attributesAllowed)) { + $attributesAllowed = array($attributesAllowed); + } + + // Store each attribute as allowed + foreach ($attributesAllowed as $attribute) { + if (is_string($attribute)) { + // Canonicalize the attribute name + $attributeName = strtolower($attribute); + $this->attributesAllowed[$attributeName] = null; + } + } + + return $this; + } + + /** + * Defined by Zend\Filter\FilterInterface + * + * If the value provided is non-scalar, the value will remain unfiltered + * + * @todo improve docblock descriptions + * @param string $value + * @return string|mixed + */ + public function filter($value) + { + if (!is_scalar($value)) { + return $value; + } + $value = (string) $value; + + // Strip HTML comments first + while (strpos($value, '' . $link . ''; + } + + return $link; + } + + /** + * Render link elements as string + * + * @param string|int $indent + * @return string + */ + public function toString($indent = null) + { + $indent = (null !== $indent) + ? $this->getWhitespace($indent) + : $this->getIndent(); + + $items = array(); + $this->getContainer()->ksort(); + foreach ($this as $item) { + $items[] = $this->itemToString($item); + } + + return $indent . implode($this->escape($this->getSeparator()) . $indent, $items); + } + + /** + * Create data item for stack + * + * @param array $attributes + * @return stdClass + */ + public function createData(array $attributes) + { + return (object) $attributes; + } + + /** + * Create item for stylesheet link item + * + * @param array $args + * @return stdClass|false Returns false if stylesheet is a duplicate + */ + public function createDataStylesheet(array $args) + { + $rel = 'stylesheet'; + $type = 'text/css'; + $media = 'screen'; + $conditionalStylesheet = false; + $href = array_shift($args); + + if ($this->isDuplicateStylesheet($href)) { + return false; + } + + if (0 < count($args)) { + $media = array_shift($args); + if (is_array($media)) { + $media = implode(',', $media); + } else { + $media = (string) $media; + } + } + if (0 < count($args)) { + $conditionalStylesheet = array_shift($args); + if (!empty($conditionalStylesheet) && is_string($conditionalStylesheet)) { + $conditionalStylesheet = (string) $conditionalStylesheet; + } else { + $conditionalStylesheet = null; + } + } + + if (0 < count($args) && is_array($args[0])) { + $extras = array_shift($args); + $extras = (array) $extras; + } + + $attributes = compact('rel', 'type', 'href', 'media', 'conditionalStylesheet', 'extras'); + + return $this->createData($attributes); + } + + /** + * Is the linked stylesheet a duplicate? + * + * @param string $uri + * @return bool + */ + protected function isDuplicateStylesheet($uri) + { + foreach ($this->getContainer() as $item) { + if (($item->rel == 'stylesheet') && ($item->href == $uri)) { + return true; + } + } + + return false; + } + + /** + * Create item for alternate link item + * + * @param array $args + * @throws Exception\InvalidArgumentException + * @return stdClass + */ + public function createDataAlternate(array $args) + { + if (3 > count($args)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Alternate tags require 3 arguments; %s provided', + count($args) + )); + } + + $rel = 'alternate'; + $href = array_shift($args); + $type = array_shift($args); + $title = array_shift($args); + + if (0 < count($args) && is_array($args[0])) { + $extras = array_shift($args); + $extras = (array) $extras; + + if (isset($extras['media']) && is_array($extras['media'])) { + $extras['media'] = implode(',', $extras['media']); + } + } + + $href = (string) $href; + $type = (string) $type; + $title = (string) $title; + + $attributes = compact('rel', 'href', 'type', 'title', 'extras'); + + return $this->createData($attributes); + } + + /** + * Create item for a prev relationship (mainly used for pagination) + * + * @param array $args + * @return stdClass + */ + public function createDataPrev(array $args) + { + $rel = 'prev'; + $href = (string) array_shift($args); + + $attributes = compact('rel', 'href'); + + return $this->createData($attributes); + } + + /** + * Create item for a prev relationship (mainly used for pagination) + * + * @param array $args + * @return stdClass + */ + public function createDataNext(array $args) + { + $rel = 'next'; + $href = (string) array_shift($args); + + $attributes = compact('rel', 'href'); + + return $this->createData($attributes); + } +} diff --git a/library/Zend/View/Helper/HeadMeta.php b/library/Zend/View/Helper/HeadMeta.php new file mode 100755 index 0000000000..66a0dff59a --- /dev/null +++ b/library/Zend/View/Helper/HeadMeta.php @@ -0,0 +1,453 @@ +setSeparator(PHP_EOL); + } + + /** + * Retrieve object instance; optionally add meta tag + * + * @param string $content + * @param string $keyValue + * @param string $keyType + * @param array $modifiers + * @param string $placement + * @return HeadMeta + */ + public function __invoke($content = null, $keyValue = null, $keyType = 'name', $modifiers = array(), $placement = Placeholder\Container\AbstractContainer::APPEND) + { + if ((null !== $content) && (null !== $keyValue)) { + $item = $this->createData($keyType, $keyValue, $content, $modifiers); + $action = strtolower($placement); + switch ($action) { + case 'append': + case 'prepend': + case 'set': + $this->$action($item); + break; + default: + $this->append($item); + break; + } + } + + return $this; + } + + /** + * Overload method access + * + * @param string $method + * @param array $args + * @throws Exception\BadMethodCallException + * @return HeadMeta + */ + public function __call($method, $args) + { + if (preg_match('/^(?Pset|(pre|ap)pend|offsetSet)(?PName|HttpEquiv|Property|Itemprop)$/', $method, $matches)) { + $action = $matches['action']; + $type = $this->normalizeType($matches['type']); + $argc = count($args); + $index = null; + + if ('offsetSet' == $action) { + if (0 < $argc) { + $index = array_shift($args); + --$argc; + } + } + + if (2 > $argc) { + throw new Exception\BadMethodCallException( + 'Too few arguments provided; requires key value, and content' + ); + } + + if (3 > $argc) { + $args[] = array(); + } + + $item = $this->createData($type, $args[0], $args[1], $args[2]); + + if ('offsetSet' == $action) { + return $this->offsetSet($index, $item); + } + + $this->$action($item); + + return $this; + } + + return parent::__call($method, $args); + } + + /** + * Render placeholder as string + * + * @param string|int $indent + * @return string + */ + public function toString($indent = null) + { + $indent = (null !== $indent) + ? $this->getWhitespace($indent) + : $this->getIndent(); + + $items = array(); + $this->getContainer()->ksort(); + + try { + foreach ($this as $item) { + $items[] = $this->itemToString($item); + } + } catch (Exception\InvalidArgumentException $e) { + trigger_error($e->getMessage(), E_USER_WARNING); + return ''; + } + + return $indent . implode($this->escape($this->getSeparator()) . $indent, $items); + } + + /** + * Create data item for inserting into stack + * + * @param string $type + * @param string $typeValue + * @param string $content + * @param array $modifiers + * @return stdClass + */ + public function createData($type, $typeValue, $content, array $modifiers) + { + $data = new stdClass; + $data->type = $type; + $data->$type = $typeValue; + $data->content = $content; + $data->modifiers = $modifiers; + + return $data; + } + + /** + * Build meta HTML string + * + * @param stdClass $item + * @throws Exception\InvalidArgumentException + * @return string + */ + public function itemToString(stdClass $item) + { + if (!in_array($item->type, $this->typeKeys)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid type "%s" provided for meta', + $item->type + )); + } + $type = $item->type; + + $modifiersString = ''; + foreach ($item->modifiers as $key => $value) { + if ($this->view->plugin('doctype')->isHtml5() + && $key == 'scheme' + ) { + throw new Exception\InvalidArgumentException( + 'Invalid modifier "scheme" provided; not supported by HTML5' + ); + } + if (!in_array($key, $this->modifierKeys)) { + continue; + } + $modifiersString .= $key . '="' . $this->escape($value) . '" '; + } + + $modifiersString = rtrim($modifiersString); + + if ('' != $modifiersString) { + $modifiersString = ' ' . $modifiersString; + } + + if (method_exists($this->view, 'plugin')) { + if ($this->view->plugin('doctype')->isHtml5() + && $type == 'charset' + ) { + $tpl = ($this->view->plugin('doctype')->isXhtml()) + ? '' + : ''; + } elseif ($this->view->plugin('doctype')->isXhtml()) { + $tpl = ''; + } else { + $tpl = ''; + } + } else { + $tpl = ''; + } + + $meta = sprintf( + $tpl, + $type, + $this->escape($item->$type), + $this->escape($item->content), + $modifiersString + ); + + if (isset($item->modifiers['conditional']) && !empty($item->modifiers['conditional']) && is_string($item->modifiers['conditional'])) { + // inner wrap with comment end and start if !IE + if (str_replace(' ', '', $item->modifiers['conditional']) === '!IE') { + $meta = '' . $meta . ''; + } + + return $meta; + } + + /** + * Normalize type attribute of meta + * + * @param string $type type in CamelCase + * @throws Exception\DomainException + * @return string + */ + protected function normalizeType($type) + { + switch ($type) { + case 'Name': + return 'name'; + case 'HttpEquiv': + return 'http-equiv'; + case 'Property': + return 'property'; + case 'Itemprop': + return 'itemprop'; + default: + throw new Exception\DomainException(sprintf( + 'Invalid type "%s" passed to normalizeType', + $type + )); + } + } + + /** + * Determine if item is valid + * + * @param mixed $item + * @return bool + */ + protected function isValid($item) + { + if ((!$item instanceof stdClass) + || !isset($item->type) + || !isset($item->modifiers) + ) { + return false; + } + + if (!isset($item->content) + && (! $this->view->plugin('doctype')->isHtml5() + || (! $this->view->plugin('doctype')->isHtml5() && $item->type !== 'charset')) + ) { + return false; + } + + // is only supported with doctype html + if (! $this->view->plugin('doctype')->isHtml5() + && $item->type === 'itemprop' + ) { + return false; + } + + // is only supported with doctype RDFa + if (!$this->view->plugin('doctype')->isRdfa() + && $item->type === 'property' + ) { + return false; + } + + return true; + } + + /** + * Append + * + * @param string $value + * @return void + * @throws Exception\InvalidArgumentException + */ + public function append($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid value passed to append; please use appendMeta()' + ); + } + + return $this->getContainer()->append($value); + } + + /** + * OffsetSet + * + * @param string|int $index + * @param string $value + * @throws Exception\InvalidArgumentException + * @return void + */ + public function offsetSet($index, $value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid value passed to offsetSet; please use offsetSetName() or offsetSetHttpEquiv()' + ); + } + + return $this->getContainer()->offsetSet($index, $value); + } + + /** + * OffsetUnset + * + * @param string|int $index + * @throws Exception\InvalidArgumentException + * @return void + */ + public function offsetUnset($index) + { + if (!in_array($index, $this->getContainer()->getKeys())) { + throw new Exception\InvalidArgumentException('Invalid index passed to offsetUnset()'); + } + + return $this->getContainer()->offsetUnset($index); + } + + /** + * Prepend + * + * @param string $value + * @throws Exception\InvalidArgumentException + * @return void + */ + public function prepend($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid value passed to prepend; please use prependMeta()' + ); + } + + return $this->getContainer()->prepend($value); + } + + /** + * Set + * + * @param string $value + * @throws Exception\InvalidArgumentException + * @return void + */ + public function set($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException('Invalid value passed to set; please use setMeta()'); + } + + $container = $this->getContainer(); + foreach ($container->getArrayCopy() as $index => $item) { + if ($item->type == $value->type && $item->{$item->type} == $value->{$value->type}) { + $this->offsetUnset($index); + } + } + + return $this->append($value); + } + + /** + * Create an HTML5-style meta charset tag. Something like + * + * Not valid in a non-HTML5 doctype + * + * @param string $charset + * @return HeadMeta Provides a fluent interface + */ + public function setCharset($charset) + { + $item = new stdClass; + $item->type = 'charset'; + $item->charset = $charset; + $item->content = null; + $item->modifiers = array(); + $this->set($item); + + return $this; + } +} diff --git a/library/Zend/View/Helper/HeadScript.php b/library/Zend/View/Helper/HeadScript.php new file mode 100755 index 0000000000..eca0475d30 --- /dev/null +++ b/library/Zend/View/Helper/HeadScript.php @@ -0,0 +1,507 @@ +setSeparator(PHP_EOL); + } + + /** + * Return headScript object + * + * Returns headScript helper object; optionally, allows specifying a script + * or script file to include. + * + * @param string $mode Script or file + * @param string $spec Script/url + * @param string $placement Append, prepend, or set + * @param array $attrs Array of script attributes + * @param string $type Script type and/or array of script attributes + * @return HeadScript + */ + public function __invoke($mode = self::FILE, $spec = null, $placement = 'APPEND', array $attrs = array(), $type = 'text/javascript') + { + if ((null !== $spec) && is_string($spec)) { + $action = ucfirst(strtolower($mode)); + $placement = strtolower($placement); + switch ($placement) { + case 'set': + case 'prepend': + case 'append': + $action = $placement . $action; + break; + default: + $action = 'append' . $action; + break; + } + $this->$action($spec, $type, $attrs); + } + + return $this; + } + + /** + * Overload method access + * + * @param string $method Method to call + * @param array $args Arguments of method + * @throws Exception\BadMethodCallException if too few arguments or invalid method + * @return HeadScript + */ + public function __call($method, $args) + { + if (preg_match('/^(?Pset|(ap|pre)pend|offsetSet)(?PFile|Script)$/', $method, $matches)) { + if (1 > count($args)) { + throw new Exception\BadMethodCallException(sprintf( + 'Method "%s" requires at least one argument', + $method + )); + } + + $action = $matches['action']; + $mode = strtolower($matches['mode']); + $type = 'text/javascript'; + $attrs = array(); + + if ('offsetSet' == $action) { + $index = array_shift($args); + if (1 > count($args)) { + throw new Exception\BadMethodCallException(sprintf( + 'Method "%s" requires at least two arguments, an index and source', + $method + )); + } + } + + $content = $args[0]; + + if (isset($args[1])) { + $type = (string) $args[1]; + } + if (isset($args[2])) { + $attrs = (array) $args[2]; + } + + switch ($mode) { + case 'script': + $item = $this->createData($type, $attrs, $content); + if ('offsetSet' == $action) { + $this->offsetSet($index, $item); + } else { + $this->$action($item); + } + break; + case 'file': + default: + if (!$this->isDuplicate($content)) { + $attrs['src'] = $content; + $item = $this->createData($type, $attrs); + if ('offsetSet' == $action) { + $this->offsetSet($index, $item); + } else { + $this->$action($item); + } + } + break; + } + + return $this; + } + + return parent::__call($method, $args); + } + + /** + * Retrieve string representation + * + * @param string|int $indent Amount of whitespaces or string to use for indention + * @return string + */ + public function toString($indent = null) + { + $indent = (null !== $indent) + ? $this->getWhitespace($indent) + : $this->getIndent(); + + if ($this->view) { + $useCdata = $this->view->plugin('doctype')->isXhtml() ? true : false; + } else { + $useCdata = $this->useCdata ? true : false; + } + + $escapeStart = ($useCdata) ? '//' : '//-->'; + + $items = array(); + $this->getContainer()->ksort(); + foreach ($this as $item) { + if (!$this->isValid($item)) { + continue; + } + + $items[] = $this->itemToString($item, $indent, $escapeStart, $escapeEnd); + } + + return implode($this->getSeparator(), $items); + } + + /** + * Start capture action + * + * @param mixed $captureType Type of capture + * @param string $type Type of script + * @param array $attrs Attributes of capture + * @throws Exception\RuntimeException + * @return void + */ + public function captureStart($captureType = Placeholder\Container\AbstractContainer::APPEND, $type = 'text/javascript', $attrs = array()) + { + if ($this->captureLock) { + throw new Exception\RuntimeException('Cannot nest headScript captures'); + } + + $this->captureLock = true; + $this->captureType = $captureType; + $this->captureScriptType = $type; + $this->captureScriptAttrs = $attrs; + ob_start(); + } + + /** + * End capture action and store + * + * @return void + */ + public function captureEnd() + { + $content = ob_get_clean(); + $type = $this->captureScriptType; + $attrs = $this->captureScriptAttrs; + $this->captureScriptType = null; + $this->captureScriptAttrs = null; + $this->captureLock = false; + + switch ($this->captureType) { + case Placeholder\Container\AbstractContainer::SET: + case Placeholder\Container\AbstractContainer::PREPEND: + case Placeholder\Container\AbstractContainer::APPEND: + $action = strtolower($this->captureType) . 'Script'; + break; + default: + $action = 'appendScript'; + break; + } + + $this->$action($content, $type, $attrs); + } + + /** + * Create data item containing all necessary components of script + * + * @param string $type Type of data + * @param array $attributes Attributes of data + * @param string $content Content of data + * @return stdClass + */ + public function createData($type, array $attributes, $content = null) + { + $data = new stdClass(); + $data->type = $type; + $data->attributes = $attributes; + $data->source = $content; + + return $data; + } + + /** + * Is the file specified a duplicate? + * + * @param string $file Name of file to check + * @return bool + */ + protected function isDuplicate($file) + { + foreach ($this->getContainer() as $item) { + if (($item->source === null) && array_key_exists('src', $item->attributes) && ($file == $item->attributes['src'])) { + return true; + } + } + + return false; + } + + /** + * Is the script provided valid? + * + * @param mixed $value Is the given script valid? + * @return bool + */ + protected function isValid($value) + { + if ((!$value instanceof stdClass) || !isset($value->type) || (!isset($value->source) && !isset($value->attributes))) { + return false; + } + + return true; + } + + /** + * Create script HTML + * + * @param mixed $item Item to convert + * @param string $indent String to add before the item + * @param string $escapeStart Starting sequence + * @param string $escapeEnd Ending sequence + * @return string + */ + public function itemToString($item, $indent, $escapeStart, $escapeEnd) + { + $attrString = ''; + if (!empty($item->attributes)) { + foreach ($item->attributes as $key => $value) { + if ((!$this->arbitraryAttributesAllowed() && !in_array($key, $this->optionalAttributes)) + || in_array($key, array('conditional', 'noescape'))) { + continue; + } + if ('defer' == $key) { + $value = 'defer'; + } + $attrString .= sprintf(' %s="%s"', $key, ($this->autoEscape) ? $this->escape($value) : $value); + } + } + + $addScriptEscape = !(isset($item->attributes['noescape']) && filter_var($item->attributes['noescape'], FILTER_VALIDATE_BOOLEAN)); + + $type = ($this->autoEscape) ? $this->escape($item->type) : $item->type; + $html = ''; + + if (isset($item->attributes['conditional']) && !empty($item->attributes['conditional']) && is_string($item->attributes['conditional'])) { + // inner wrap with comment end and start if !IE + if (str_replace(' ', '', $item->attributes['conditional']) === '!IE') { + $html = '' . $html . ''; + } else { + $html = $indent . $html; + } + + return $html; + } + + /** + * Override append + * + * @param string $value Append script or file + * @throws Exception\InvalidArgumentException + * @return void + */ + public function append($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid argument passed to append(); please use one of the helper methods, appendScript() or appendFile()' + ); + } + + return $this->getContainer()->append($value); + } + + /** + * Override prepend + * + * @param string $value Prepend script or file + * @throws Exception\InvalidArgumentException + * @return void + */ + public function prepend($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid argument passed to prepend(); please use one of the helper methods, prependScript() or prependFile()' + ); + } + + return $this->getContainer()->prepend($value); + } + + /** + * Override set + * + * @param string $value Set script or file + * @throws Exception\InvalidArgumentException + * @return void + */ + public function set($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid argument passed to set(); please use one of the helper methods, setScript() or setFile()' + ); + } + + return $this->getContainer()->set($value); + } + + /** + * Override offsetSet + * + * @param string|int $index Set script of file offset + * @param mixed $value + * @throws Exception\InvalidArgumentException + * @return void + */ + public function offsetSet($index, $value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid argument passed to offsetSet(); please use one of the helper methods, offsetSetScript() or offsetSetFile()' + ); + } + + return $this->getContainer()->offsetSet($index, $value); + } + + /** + * Set flag indicating if arbitrary attributes are allowed + * + * @param bool $flag Set flag + * @return HeadScript + */ + public function setAllowArbitraryAttributes($flag) + { + $this->arbitraryAttributes = (bool) $flag; + return $this; + } + + /** + * Are arbitrary attributes allowed? + * + * @return bool + */ + public function arbitraryAttributesAllowed() + { + return $this->arbitraryAttributes; + } +} diff --git a/library/Zend/View/Helper/HeadStyle.php b/library/Zend/View/Helper/HeadStyle.php new file mode 100755 index 0000000000..c6cce3a708 --- /dev/null +++ b/library/Zend/View/Helper/HeadStyle.php @@ -0,0 +1,413 @@ +setSeparator(PHP_EOL); + } + + /** + * Return headStyle object + * + * Returns headStyle helper object; optionally, allows specifying + * + * @param string $content Stylesheet contents + * @param string $placement Append, prepend, or set + * @param string|array $attributes Optional attributes to utilize + * @return HeadStyle + */ + public function __invoke($content = null, $placement = 'APPEND', $attributes = array()) + { + if ((null !== $content) && is_string($content)) { + switch (strtoupper($placement)) { + case 'SET': + $action = 'setStyle'; + break; + case 'PREPEND': + $action = 'prependStyle'; + break; + case 'APPEND': + default: + $action = 'appendStyle'; + break; + } + $this->$action($content, $attributes); + } + + return $this; + } + + /** + * Overload method calls + * + * @param string $method + * @param array $args + * @throws Exception\BadMethodCallException When no $content provided or invalid method + * @return void + */ + public function __call($method, $args) + { + if (preg_match('/^(?Pset|(ap|pre)pend|offsetSet)(Style)$/', $method, $matches)) { + $index = null; + $argc = count($args); + $action = $matches['action']; + + if ('offsetSet' == $action) { + if (0 < $argc) { + $index = array_shift($args); + --$argc; + } + } + + if (1 > $argc) { + throw new Exception\BadMethodCallException(sprintf( + 'Method "%s" requires minimally content for the stylesheet', + $method + )); + } + + $content = $args[0]; + $attrs = array(); + if (isset($args[1])) { + $attrs = (array) $args[1]; + } + + $item = $this->createData($content, $attrs); + + if ('offsetSet' == $action) { + $this->offsetSet($index, $item); + } else { + $this->$action($item); + } + + return $this; + } + + return parent::__call($method, $args); + } + + /** + * Create string representation of placeholder + * + * @param string|int $indent + * @return string + */ + public function toString($indent = null) + { + $indent = (null !== $indent) + ? $this->getWhitespace($indent) + : $this->getIndent(); + + $items = array(); + $this->getContainer()->ksort(); + foreach ($this as $item) { + if (!$this->isValid($item)) { + continue; + } + $items[] = $this->itemToString($item, $indent); + } + + $return = $indent . implode($this->getSeparator() . $indent, $items); + $return = preg_replace("/(\r\n?|\n)/", '$1' . $indent, $return); + + return $return; + } + + /** + * Start capture action + * + * @param string $type + * @param string $attrs + * @throws Exception\RuntimeException + * @return void + */ + public function captureStart($type = Placeholder\Container\AbstractContainer::APPEND, $attrs = null) + { + if ($this->captureLock) { + throw new Exception\RuntimeException('Cannot nest headStyle captures'); + } + + $this->captureLock = true; + $this->captureAttrs = $attrs; + $this->captureType = $type; + ob_start(); + } + + /** + * End capture action and store + * + * @return void + */ + public function captureEnd() + { + $content = ob_get_clean(); + $attrs = $this->captureAttrs; + $this->captureAttrs = null; + $this->captureLock = false; + + switch ($this->captureType) { + case Placeholder\Container\AbstractContainer::SET: + $this->setStyle($content, $attrs); + break; + case Placeholder\Container\AbstractContainer::PREPEND: + $this->prependStyle($content, $attrs); + break; + case Placeholder\Container\AbstractContainer::APPEND: + default: + $this->appendStyle($content, $attrs); + break; + } + } + + /** + * Create data item for use in stack + * + * @param string $content + * @param array $attributes + * @return stdClass + */ + public function createData($content, array $attributes) + { + if (!isset($attributes['media'])) { + $attributes['media'] = 'screen'; + } elseif (is_array($attributes['media'])) { + $attributes['media'] = implode(',', $attributes['media']); + } + + $data = new stdClass(); + $data->content = $content; + $data->attributes = $attributes; + + return $data; + } + + /** + * Determine if a value is a valid style tag + * + * @param mixed $value + * @return bool + */ + protected function isValid($value) + { + if ((!$value instanceof stdClass) || !isset($value->content) || !isset($value->attributes)) { + return false; + } + + return true; + } + + /** + * Convert content and attributes into valid style tag + * + * @param stdClass $item Item to render + * @param string $indent Indentation to use + * @return string + */ + public function itemToString(stdClass $item, $indent) + { + $attrString = ''; + if (!empty($item->attributes)) { + $enc = 'UTF-8'; + if ($this->view instanceof View\Renderer\RendererInterface + && method_exists($this->view, 'getEncoding') + ) { + $enc = $this->view->getEncoding(); + } + $escaper = $this->getEscaper($enc); + foreach ($item->attributes as $key => $value) { + if (!in_array($key, $this->optionalAttributes)) { + continue; + } + if ('media' == $key) { + if (false === strpos($value, ',')) { + if (!in_array($value, $this->mediaTypes)) { + continue; + } + } else { + $mediaTypes = explode(',', $value); + $value = ''; + foreach ($mediaTypes as $type) { + $type = trim($type); + if (!in_array($type, $this->mediaTypes)) { + continue; + } + $value .= $type .','; + } + $value = substr($value, 0, -1); + } + } + $attrString .= sprintf(' %s="%s"', $key, $escaper->escapeHtmlAttr($value)); + } + } + + $escapeStart = $indent . '' . PHP_EOL; + if (isset($item->attributes['conditional']) + && !empty($item->attributes['conditional']) + && is_string($item->attributes['conditional']) + ) { + $escapeStart = null; + $escapeEnd = null; + } + + $html = ''; + + if (null == $escapeStart && null == $escapeEnd) { + // inner wrap with comment end and start if !IE + if (str_replace(' ', '', $item->attributes['conditional']) === '!IE') { + $html = '' . $html . ''; + } + + return $html; + } + + /** + * Override append to enforce style creation + * + * @param mixed $value + * @throws Exception\InvalidArgumentException + * @return void + */ + public function append($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid value passed to append; please use appendStyle()' + ); + } + + return $this->getContainer()->append($value); + } + + /** + * Override offsetSet to enforce style creation + * + * @param string|int $index + * @param mixed $value + * @throws Exception\InvalidArgumentException + * @return void + */ + public function offsetSet($index, $value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid value passed to offsetSet; please use offsetSetStyle()' + ); + } + + return $this->getContainer()->offsetSet($index, $value); + } + + /** + * Override prepend to enforce style creation + * + * @param mixed $value + * @throws Exception\InvalidArgumentException + * @return void + */ + public function prepend($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException( + 'Invalid value passed to prepend; please use prependStyle()' + ); + } + + return $this->getContainer()->prepend($value); + } + + /** + * Override set to enforce style creation + * + * @param mixed $value + * @throws Exception\InvalidArgumentException + * @return void + */ + public function set($value) + { + if (!$this->isValid($value)) { + throw new Exception\InvalidArgumentException('Invalid value passed to set; please use setStyle()'); + } + + return $this->getContainer()->set($value); + } +} diff --git a/library/Zend/View/Helper/HeadTitle.php b/library/Zend/View/Helper/HeadTitle.php new file mode 100755 index 0000000000..2ea8d78dd0 --- /dev/null +++ b/library/Zend/View/Helper/HeadTitle.php @@ -0,0 +1,263 @@ +getDefaultAttachOrder()) + ? Placeholder\Container\AbstractContainer::APPEND + : $this->getDefaultAttachOrder(); + } + + $title = (string) $title; + if ($title !== '') { + if ($setType == Placeholder\Container\AbstractContainer::SET) { + $this->set($title); + } elseif ($setType == Placeholder\Container\AbstractContainer::PREPEND) { + $this->prepend($title); + } else { + $this->append($title); + } + } + + return $this; + } + + /** + * Render title (wrapped by title tag) + * + * @param string|null $indent + * @return string + */ + public function toString($indent = null) + { + $indent = (null !== $indent) + ? $this->getWhitespace($indent) + : $this->getIndent(); + + $output = $this->renderTitle(); + + return $indent . '' . $output . ''; + } + + /** + * Render title string + * + * @return string + */ + public function renderTitle() + { + $items = array(); + + if (null !== ($translator = $this->getTranslator())) { + foreach ($this as $item) { + $items[] = $translator->translate($item, $this->getTranslatorTextDomain()); + } + } else { + foreach ($this as $item) { + $items[] = $item; + } + } + + $separator = $this->getSeparator(); + $output = ''; + + $prefix = $this->getPrefix(); + if ($prefix) { + $output .= $prefix; + } + + $output .= implode($separator, $items); + + $postfix = $this->getPostfix(); + if ($postfix) { + $output .= $postfix; + } + + $output = ($this->autoEscape) ? $this->escape($output) : $output; + + return $output; + } + + /** + * Set a default order to add titles + * + * @param string $setType + * @throws Exception\DomainException + * @return HeadTitle + */ + public function setDefaultAttachOrder($setType) + { + if (!in_array($setType, array( + Placeholder\Container\AbstractContainer::APPEND, + Placeholder\Container\AbstractContainer::SET, + Placeholder\Container\AbstractContainer::PREPEND + ))) { + throw new Exception\DomainException( + "You must use a valid attach order: 'PREPEND', 'APPEND' or 'SET'" + ); + } + $this->defaultAttachOrder = $setType; + + return $this; + } + + /** + * Get the default attach order, if any. + * + * @return mixed + */ + public function getDefaultAttachOrder() + { + return $this->defaultAttachOrder; + } + + // Translator methods - Good candidate to refactor as a trait with PHP 5.4 + + /** + * Sets translator to use in helper + * + * @param Translator $translator [optional] translator. + * Default is null, which sets no translator. + * @param string $textDomain [optional] text domain + * Default is null, which skips setTranslatorTextDomain + * @return HeadTitle + */ + public function setTranslator(Translator $translator = null, $textDomain = null) + { + $this->translator = $translator; + if (null !== $textDomain) { + $this->setTranslatorTextDomain($textDomain); + } + return $this; + } + + /** + * Returns translator used in helper + * + * @return Translator|null + */ + public function getTranslator() + { + if (! $this->isTranslatorEnabled()) { + return null; + } + + return $this->translator; + } + + /** + * Checks if the helper has a translator + * + * @return bool + */ + public function hasTranslator() + { + return (bool) $this->getTranslator(); + } + + /** + * Sets whether translator is enabled and should be used + * + * @param bool $enabled [optional] whether translator should be used. + * Default is true. + * @return HeadTitle + */ + public function setTranslatorEnabled($enabled = true) + { + $this->translatorEnabled = (bool) $enabled; + return $this; + } + + /** + * Returns whether translator is enabled and should be used + * + * @return bool + */ + public function isTranslatorEnabled() + { + return $this->translatorEnabled; + } + + /** + * Set translation text domain + * + * @param string $textDomain + * @return HeadTitle + */ + public function setTranslatorTextDomain($textDomain = 'default') + { + $this->translatorTextDomain = $textDomain; + return $this; + } + + /** + * Return the translation text domain + * + * @return string + */ + public function getTranslatorTextDomain() + { + return $this->translatorTextDomain; + } +} diff --git a/library/Zend/View/Helper/HelperInterface.php b/library/Zend/View/Helper/HelperInterface.php new file mode 100755 index 0000000000..2d58c01f90 --- /dev/null +++ b/library/Zend/View/Helper/HelperInterface.php @@ -0,0 +1,30 @@ + $data, 'quality' => 'high'), $params); + + $htmlObject = $this->getView()->plugin('htmlObject'); + return $htmlObject($data, self::TYPE, $attribs, $params, $content); + } +} diff --git a/library/Zend/View/Helper/HtmlList.php b/library/Zend/View/Helper/HtmlList.php new file mode 100755 index 0000000000..29c79c73c1 --- /dev/null +++ b/library/Zend/View/Helper/HtmlList.php @@ -0,0 +1,58 @@ +getView()->plugin('escapeHtml'); + $item = $escaper($item); + } + $list .= '
  • ' . $item . '
  • ' . self::EOL; + } else { + $itemLength = 5 + strlen(self::EOL); + if ($itemLength < strlen($list)) { + $list = substr($list, 0, strlen($list) - $itemLength) + . $this($item, $ordered, $attribs, $escape) . '' . self::EOL; + } else { + $list .= '
  • ' . $this($item, $ordered, $attribs, $escape) . '
  • ' . self::EOL; + } + } + } + + if ($attribs) { + $attribs = $this->htmlAttribs($attribs); + } else { + $attribs = ''; + } + + $tag = ($ordered) ? 'ol' : 'ul'; + + return '<' . $tag . $attribs . '>' . self::EOL . $list . '' . self::EOL; + } +} diff --git a/library/Zend/View/Helper/HtmlObject.php b/library/Zend/View/Helper/HtmlObject.php new file mode 100755 index 0000000000..17c2822f35 --- /dev/null +++ b/library/Zend/View/Helper/HtmlObject.php @@ -0,0 +1,63 @@ + $data, 'type' => $type), $attribs); + + // Params + $paramHtml = array(); + $closingBracket = $this->getClosingBracket(); + + foreach ($params as $param => $options) { + if (is_string($options)) { + $options = array('value' => $options); + } + + $options = array_merge(array('name' => $param), $options); + + $paramHtml[] = 'htmlAttribs($options) . $closingBracket; + } + + // Content + if (is_array($content)) { + $content = implode(self::EOL, $content); + } + + // Object header + $xhtml = 'htmlAttribs($attribs) . '>' . self::EOL + . implode(self::EOL, $paramHtml) . self::EOL + . ($content ? $content . self::EOL : '') + . ''; + + return $xhtml; + } +} diff --git a/library/Zend/View/Helper/HtmlPage.php b/library/Zend/View/Helper/HtmlPage.php new file mode 100755 index 0000000000..ee170c1b0c --- /dev/null +++ b/library/Zend/View/Helper/HtmlPage.php @@ -0,0 +1,51 @@ + self::ATTRIB_CLASSID); + + /** + * Output a html object tag + * + * @param string $data The html url + * @param array $attribs Attribs for the object tag + * @param array $params Params for in the object tag + * @param string $content Alternative content + * @return string + */ + public function __invoke($data, array $attribs = array(), array $params = array(), $content = null) + { + // Attribs + $attribs = array_merge($this->attribs, $attribs); + + // Params + $params = array_merge(array('data' => $data), $params); + + $htmlObject = $this->getView()->plugin('htmlObject'); + return $htmlObject($data, self::TYPE, $attribs, $params, $content); + } +} diff --git a/library/Zend/View/Helper/HtmlQuicktime.php b/library/Zend/View/Helper/HtmlQuicktime.php new file mode 100755 index 0000000000..629d6efa32 --- /dev/null +++ b/library/Zend/View/Helper/HtmlQuicktime.php @@ -0,0 +1,56 @@ + self::ATTRIB_CLASSID, 'codebase' => self::ATTRIB_CODEBASE); + + /** + * Output a quicktime movie object tag + * + * @param string $data The quicktime file + * @param array $attribs Attribs for the object tag + * @param array $params Params for in the object tag + * @param string $content Alternative content + * @return string + */ + public function __invoke($data, array $attribs = array(), array $params = array(), $content = null) + { + // Attrs + $attribs = array_merge($this->attribs, $attribs); + + // Params + $params = array_merge(array('src' => $data), $params); + + $htmlObject = $this->getView()->plugin('htmlObject'); + return $htmlObject($data, self::TYPE, $attribs, $params, $content); + } +} diff --git a/library/Zend/View/Helper/Identity.php b/library/Zend/View/Helper/Identity.php new file mode 100755 index 0000000000..4fe1453736 --- /dev/null +++ b/library/Zend/View/Helper/Identity.php @@ -0,0 +1,69 @@ +authenticationService instanceof AuthenticationService) { + throw new Exception\RuntimeException('No AuthenticationService instance provided'); + } + + if (!$this->authenticationService->hasIdentity()) { + return null; + } + + return $this->authenticationService->getIdentity(); + } + + /** + * Set AuthenticationService instance + * + * @param AuthenticationService $authenticationService + * @return Identity + */ + public function setAuthenticationService(AuthenticationService $authenticationService) + { + $this->authenticationService = $authenticationService; + return $this; + } + + /** + * Get AuthenticationService instance + * + * @return AuthenticationService + */ + public function getAuthenticationService() + { + return $this->authenticationService; + } +} diff --git a/library/Zend/View/Helper/InlineScript.php b/library/Zend/View/Helper/InlineScript.php new file mode 100755 index 0000000000..57dace7d6f --- /dev/null +++ b/library/Zend/View/Helper/InlineScript.php @@ -0,0 +1,42 @@ +response instanceof Response) { + $headers = $this->response->getHeaders(); + $headers->addHeaderLine('Content-Type', 'application/json'); + } + + return $data; + } + + /** + * Set the response object + * + * @param Response $response + * @return Json + */ + public function setResponse(Response $response) + { + $this->response = $response; + return $this; + } +} diff --git a/library/Zend/View/Helper/Layout.php b/library/Zend/View/Helper/Layout.php new file mode 100755 index 0000000000..65b630fb24 --- /dev/null +++ b/library/Zend/View/Helper/Layout.php @@ -0,0 +1,98 @@ +getRoot(); + } + + return $this->setTemplate($template); + } + + /** + * Get layout template + * + * @return string + */ + public function getLayout() + { + return $this->getRoot()->getTemplate(); + } + + /** + * Get the root view model + * + * @throws Exception\RuntimeException + * @return null|Model + */ + protected function getRoot() + { + $helper = $this->getViewModelHelper(); + + if (!$helper->hasRoot()) { + throw new Exception\RuntimeException(sprintf( + '%s: no view model currently registered as root in renderer', + __METHOD__ + )); + } + + return $helper->getRoot(); + } + + /** + * Set layout template + * + * @param string $template + * @return Layout + */ + public function setTemplate($template) + { + $this->getRoot()->setTemplate((string) $template); + return $this; + } + + /** + * Retrieve the view model helper + * + * @return ViewModel + */ + protected function getViewModelHelper() + { + if (null === $this->viewModelHelper) { + $this->viewModelHelper = $this->getView()->plugin('view_model'); + } + + return $this->viewModelHelper; + } +} diff --git a/library/Zend/View/Helper/Navigation.php b/library/Zend/View/Helper/Navigation.php new file mode 100755 index 0000000000..015a5ec020 --- /dev/null +++ b/library/Zend/View/Helper/Navigation.php @@ -0,0 +1,346 @@ +setContainer($container); + } + + return $this; + } + + /** + * Magic overload: Proxy to other navigation helpers or the container + * + * Examples of usage from a view script or layout: + * + * // proxy to Menu helper and render container: + * echo $this->navigation()->menu(); + * + * // proxy to Breadcrumbs helper and set indentation: + * $this->navigation()->breadcrumbs()->setIndent(8); + * + * // proxy to container and find all pages with 'blog' route: + * $blogPages = $this->navigation()->findAllByRoute('blog'); + * + * + * @param string $method helper name or method name in container + * @param array $arguments [optional] arguments to pass + * @throws \Zend\View\Exception\ExceptionInterface if proxying to a helper, and the + * helper is not an instance of the + * interface specified in + * {@link findHelper()} + * @throws \Zend\Navigation\Exception\ExceptionInterface if method does not exist in container + * @return mixed returns what the proxied call returns + */ + public function __call($method, array $arguments = array()) + { + // check if call should proxy to another helper + $helper = $this->findHelper($method, false); + if ($helper) { + if ($helper instanceof ServiceLocatorAwareInterface && $this->getServiceLocator()) { + $helper->setServiceLocator($this->getServiceLocator()); + } + return call_user_func_array($helper, $arguments); + } + + // default behaviour: proxy call to container + return parent::__call($method, $arguments); + } + + /** + * Renders helper + * + * @param AbstractContainer $container + * @return string + * @throws Exception\RuntimeException + */ + public function render($container = null) + { + return $this->findHelper($this->getDefaultProxy())->render($container); + } + + /** + * Returns the helper matching $proxy + * + * The helper must implement the interface + * {@link Zend\View\Helper\Navigation\Helper}. + * + * @param string $proxy helper name + * @param bool $strict [optional] whether exceptions should be + * thrown if something goes + * wrong. Default is true. + * @throws Exception\RuntimeException if $strict is true and helper cannot be found + * @return \Zend\View\Helper\Navigation\HelperInterface helper instance + */ + public function findHelper($proxy, $strict = true) + { + $plugins = $this->getPluginManager(); + if (!$plugins->has($proxy)) { + if ($strict) { + throw new Exception\RuntimeException(sprintf( + 'Failed to find plugin for %s', + $proxy + )); + } + return false; + } + + $helper = $plugins->get($proxy); + $container = $this->getContainer(); + $hash = spl_object_hash($container) . spl_object_hash($helper); + + if (!isset($this->injected[$hash])) { + $helper->setContainer(); + $this->inject($helper); + $this->injected[$hash] = true; + } else { + if ($this->getInjectContainer()) { + $helper->setContainer($container); + } + } + + return $helper; + } + + /** + * Injects container, ACL, and translator to the given $helper if this + * helper is configured to do so + * + * @param NavigationHelper $helper helper instance + * @return void + */ + protected function inject(NavigationHelper $helper) + { + if ($this->getInjectContainer() && !$helper->hasContainer()) { + $helper->setContainer($this->getContainer()); + } + + if ($this->getInjectAcl()) { + if (!$helper->hasAcl()) { + $helper->setAcl($this->getAcl()); + } + if (!$helper->hasRole()) { + $helper->setRole($this->getRole()); + } + } + + if ($this->getInjectTranslator() && !$helper->hasTranslator()) { + $helper->setTranslator( + $this->getTranslator(), + $this->getTranslatorTextDomain() + ); + } + } + + /** + * Sets the default proxy to use in {@link render()} + * + * @param string $proxy default proxy + * @return Navigation + */ + public function setDefaultProxy($proxy) + { + $this->defaultProxy = (string) $proxy; + return $this; + } + + /** + * Returns the default proxy to use in {@link render()} + * + * @return string + */ + public function getDefaultProxy() + { + return $this->defaultProxy; + } + + /** + * Sets whether container should be injected when proxying + * + * @param bool $injectContainer + * @return Navigation + */ + public function setInjectContainer($injectContainer = true) + { + $this->injectContainer = (bool) $injectContainer; + return $this; + } + + /** + * Returns whether container should be injected when proxying + * + * @return bool + */ + public function getInjectContainer() + { + return $this->injectContainer; + } + + /** + * Sets whether ACL should be injected when proxying + * + * @param bool $injectAcl + * @return Navigation + */ + public function setInjectAcl($injectAcl = true) + { + $this->injectAcl = (bool) $injectAcl; + return $this; + } + + /** + * Returns whether ACL should be injected when proxying + * + * @return bool + */ + public function getInjectAcl() + { + return $this->injectAcl; + } + + /** + * Sets whether translator should be injected when proxying + * + * @param bool $injectTranslator + * @return Navigation + */ + public function setInjectTranslator($injectTranslator = true) + { + $this->injectTranslator = (bool) $injectTranslator; + return $this; + } + + /** + * Returns whether translator should be injected when proxying + * + * @return bool + */ + public function getInjectTranslator() + { + return $this->injectTranslator; + } + + /** + * Set manager for retrieving navigation helpers + * + * @param Navigation\PluginManager $plugins + * @return Navigation + */ + public function setPluginManager(Navigation\PluginManager $plugins) + { + $renderer = $this->getView(); + if ($renderer) { + $plugins->setRenderer($renderer); + } + $this->plugins = $plugins; + + return $this; + } + + /** + * Retrieve plugin loader for navigation helpers + * + * Lazy-loads an instance of Navigation\HelperLoader if none currently + * registered. + * + * @return Navigation\PluginManager + */ + public function getPluginManager() + { + if (null === $this->plugins) { + $this->setPluginManager(new Navigation\PluginManager()); + } + + return $this->plugins; + } + + /** + * Set the View object + * + * @param Renderer $view + * @return self + */ + public function setView(Renderer $view) + { + parent::setView($view); + if ($view && $this->plugins) { + $this->plugins->setRenderer($view); + } + return $this; + } +} diff --git a/library/Zend/View/Helper/Navigation/AbstractHelper.php b/library/Zend/View/Helper/Navigation/AbstractHelper.php new file mode 100755 index 0000000000..1b997683ff --- /dev/null +++ b/library/Zend/View/Helper/Navigation/AbstractHelper.php @@ -0,0 +1,945 @@ +getContainer(), $method), + $arguments); + } + + /** + * Magic overload: Proxy to {@link render()}. + * + * This method will trigger an E_USER_ERROR if rendering the helper causes + * an exception to be thrown. + * + * Implements {@link HelperInterface::__toString()}. + * + * @return string + */ + public function __toString() + { + try { + return $this->render(); + } catch (\Exception $e) { + $msg = get_class($e) . ': ' . $e->getMessage(); + trigger_error($msg, E_USER_ERROR); + return ''; + } + } + + /** + * Finds the deepest active page in the given container + * + * @param Navigation\AbstractContainer $container container to search + * @param int|null $minDepth [optional] minimum depth + * required for page to be + * valid. Default is to use + * {@link getMinDepth()}. A + * null value means no minimum + * depth required. + * @param int|null $maxDepth [optional] maximum depth + * a page can have to be + * valid. Default is to use + * {@link getMaxDepth()}. A + * null value means no maximum + * depth required. + * @return array an associative array with + * the values 'depth' and + * 'page', or an empty array + * if not found + */ + public function findActive($container, $minDepth = null, $maxDepth = -1) + { + $this->parseContainer($container); + if (!is_int($minDepth)) { + $minDepth = $this->getMinDepth(); + } + if ((!is_int($maxDepth) || $maxDepth < 0) && null !== $maxDepth) { + $maxDepth = $this->getMaxDepth(); + } + + $found = null; + $foundDepth = -1; + $iterator = new RecursiveIteratorIterator( + $container, + RecursiveIteratorIterator::CHILD_FIRST + ); + + /** @var \Zend\Navigation\Page\AbstractPage $page */ + foreach ($iterator as $page) { + $currDepth = $iterator->getDepth(); + if ($currDepth < $minDepth || !$this->accept($page)) { + // page is not accepted + continue; + } + + if ($page->isActive(false) && $currDepth > $foundDepth) { + // found an active page at a deeper level than before + $found = $page; + $foundDepth = $currDepth; + } + } + + if (is_int($maxDepth) && $foundDepth > $maxDepth) { + while ($foundDepth > $maxDepth) { + if (--$foundDepth < $minDepth) { + $found = null; + break; + } + + $found = $found->getParent(); + if (!$found instanceof AbstractPage) { + $found = null; + break; + } + } + } + + if ($found) { + return array('page' => $found, 'depth' => $foundDepth); + } + + return array(); + } + + /** + * Verifies container and eventually fetches it from service locator if it is a string + * + * @param Navigation\AbstractContainer|string|null $container + * @throws Exception\InvalidArgumentException + */ + protected function parseContainer(&$container = null) + { + if (null === $container) { + return; + } + + if (is_string($container)) { + if (!$this->getServiceLocator()) { + throw new Exception\InvalidArgumentException(sprintf( + 'Attempted to set container with alias "%s" but no ServiceLocator was set', + $container + )); + } + + /** + * Load the navigation container from the root service locator + * + * The navigation container is probably located in Zend\ServiceManager\ServiceManager + * and not in the View\HelperPluginManager. If the set service locator is a + * HelperPluginManager, access the navigation container via the main service locator. + */ + $sl = $this->getServiceLocator(); + if ($sl instanceof View\HelperPluginManager) { + $sl = $sl->getServiceLocator(); + } + $container = $sl->get($container); + return; + } + + if (!$container instanceof Navigation\AbstractContainer) { + throw new Exception\InvalidArgumentException( + 'Container must be a string alias or an instance of ' . + 'Zend\Navigation\AbstractContainer' + ); + } + } + + // Iterator filter methods: + + /** + * Determines whether a page should be accepted when iterating + * + * Default listener may be 'overridden' by attaching listener to 'isAllowed' + * method. Listener must be 'short circuited' if overriding default ACL + * listener. + * + * Rules: + * - If a page is not visible it is not accepted, unless RenderInvisible has + * been set to true + * - If $useAcl is true (default is true): + * - Page is accepted if listener returns true, otherwise false + * - If page is accepted and $recursive is true, the page + * will not be accepted if it is the descendant of a non-accepted page + * + * @param AbstractPage $page page to check + * @param bool $recursive [optional] if true, page will not be + * accepted if it is the descendant of + * a page that is not accepted. Default + * is true + * + * @return bool Whether page should be accepted + */ + public function accept(AbstractPage $page, $recursive = true) + { + $accept = true; + + if (!$page->isVisible(false) && !$this->getRenderInvisible()) { + $accept = false; + } elseif ($this->getUseAcl()) { + $acl = $this->getAcl(); + $role = $this->getRole(); + $params = array('acl' => $acl, 'page' => $page, 'role' => $role); + $accept = $this->isAllowed($params); + } + + if ($accept && $recursive) { + $parent = $page->getParent(); + + if ($parent instanceof AbstractPage) { + $accept = $this->accept($parent, true); + } + } + + return $accept; + } + + /** + * Determines whether a page should be allowed given certain parameters + * + * @param array $params + * @return bool + */ + protected function isAllowed($params) + { + $results = $this->getEventManager()->trigger(__FUNCTION__, $this, $params); + return $results->last(); + } + + // Util methods: + + /** + * Retrieve whitespace representation of $indent + * + * @param int|string $indent + * @return string + */ + protected function getWhitespace($indent) + { + if (is_int($indent)) { + $indent = str_repeat(' ', $indent); + } + + return (string) $indent; + } + + /** + * Converts an associative array to a string of tag attributes. + * + * Overloads {@link View\Helper\AbstractHtmlElement::htmlAttribs()}. + * + * @param array $attribs an array where each key-value pair is converted + * to an attribute name and value + * @return string + */ + protected function htmlAttribs($attribs) + { + // filter out null values and empty string values + foreach ($attribs as $key => $value) { + if ($value === null || (is_string($value) && !strlen($value))) { + unset($attribs[$key]); + } + } + + return parent::htmlAttribs($attribs); + } + + /** + * Returns an HTML string containing an 'a' element for the given page + * + * @param AbstractPage $page page to generate HTML for + * @return string HTML string (Label) + */ + public function htmlify(AbstractPage $page) + { + $label = $this->translate($page->getLabel(), $page->getTextDomain()); + $title = $this->translate($page->getTitle(), $page->getTextDomain()); + + // get attribs for anchor element + $attribs = array( + 'id' => $page->getId(), + 'title' => $title, + 'class' => $page->getClass(), + 'href' => $page->getHref(), + 'target' => $page->getTarget() + ); + + /** @var \Zend\View\Helper\EscapeHtml $escaper */ + $escaper = $this->view->plugin('escapeHtml'); + $label = $escaper($label); + + return 'htmlAttribs($attribs) . '>' . $label . ''; + } + + /** + * Translate a message (for label, title, …) + * + * @param string $message ID of the message to translate + * @param string $textDomain Text domain (category name for the translations) + * @return string Translated message + */ + protected function translate($message, $textDomain = null) + { + if (is_string($message) && !empty($message)) { + if (null !== ($translator = $this->getTranslator())) { + if (null === $textDomain) { + $textDomain = $this->getTranslatorTextDomain(); + } + + return $translator->translate($message, $textDomain); + } + } + + return $message; + } + + /** + * Normalize an ID + * + * Overrides {@link View\Helper\AbstractHtmlElement::normalizeId()}. + * + * @param string $value + * @return string + */ + protected function normalizeId($value) + { + $prefix = get_class($this); + $prefix = strtolower(trim(substr($prefix, strrpos($prefix, '\\')), '\\')); + + return $prefix . '-' . $value; + } + + /** + * Sets ACL to use when iterating pages + * + * Implements {@link HelperInterface::setAcl()}. + * + * @param Acl\AclInterface $acl ACL object. + * @return AbstractHelper + */ + public function setAcl(Acl\AclInterface $acl = null) + { + $this->acl = $acl; + return $this; + } + + /** + * Returns ACL or null if it isn't set using {@link setAcl()} or + * {@link setDefaultAcl()} + * + * Implements {@link HelperInterface::getAcl()}. + * + * @return Acl\AclInterface|null ACL object or null + */ + public function getAcl() + { + if ($this->acl === null && static::$defaultAcl !== null) { + return static::$defaultAcl; + } + + return $this->acl; + } + + /** + * Checks if the helper has an ACL instance + * + * Implements {@link HelperInterface::hasAcl()}. + * + * @return bool + */ + public function hasAcl() + { + if ($this->acl instanceof Acl\Acl + || static::$defaultAcl instanceof Acl\Acl + ) { + return true; + } + + return false; + } + + /** + * Set the event manager. + * + * @param EventManagerInterface $events + * @return AbstractHelper + */ + public function setEventManager(EventManagerInterface $events) + { + $events->setIdentifiers(array( + __CLASS__, + get_called_class(), + )); + + $this->events = $events; + + $this->setDefaultListeners(); + + return $this; + } + + /** + * Get the event manager. + * + * @return EventManagerInterface + */ + public function getEventManager() + { + if (null === $this->events) { + $this->setEventManager(new EventManager()); + } + + return $this->events; + } + + /** + * Sets navigation container the helper operates on by default + * + * Implements {@link HelperInterface::setContainer()}. + * + * @param string|Navigation\AbstractContainer $container Default is null, meaning container will be reset. + * @return AbstractHelper + */ + public function setContainer($container = null) + { + $this->parseContainer($container); + $this->container = $container; + + return $this; + } + + /** + * Returns the navigation container helper operates on by default + * + * Implements {@link HelperInterface::getContainer()}. + * + * If no container is set, a new container will be instantiated and + * stored in the helper. + * + * @return Navigation\AbstractContainer navigation container + */ + public function getContainer() + { + if (null === $this->container) { + $this->container = new Navigation\Navigation(); + } + + return $this->container; + } + + /** + * Checks if the helper has a container + * + * Implements {@link HelperInterface::hasContainer()}. + * + * @return bool + */ + public function hasContainer() + { + return null !== $this->container; + } + + /** + * Set the indentation string for using in {@link render()}, optionally a + * number of spaces to indent with + * + * @param string|int $indent + * @return AbstractHelper + */ + public function setIndent($indent) + { + $this->indent = $this->getWhitespace($indent); + return $this; + } + + /** + * Returns indentation + * + * @return string + */ + public function getIndent() + { + return $this->indent; + } + + /** + * Sets the maximum depth a page can have to be included when rendering + * + * @param int $maxDepth Default is null, which sets no maximum depth. + * @return AbstractHelper + */ + public function setMaxDepth($maxDepth = null) + { + if (null === $maxDepth || is_int($maxDepth)) { + $this->maxDepth = $maxDepth; + } else { + $this->maxDepth = (int) $maxDepth; + } + + return $this; + } + + /** + * Returns maximum depth a page can have to be included when rendering + * + * @return int|null + */ + public function getMaxDepth() + { + return $this->maxDepth; + } + + /** + * Sets the minimum depth a page must have to be included when rendering + * + * @param int $minDepth Default is null, which sets no minimum depth. + * @return AbstractHelper + */ + public function setMinDepth($minDepth = null) + { + if (null === $minDepth || is_int($minDepth)) { + $this->minDepth = $minDepth; + } else { + $this->minDepth = (int) $minDepth; + } + + return $this; + } + + /** + * Returns minimum depth a page must have to be included when rendering + * + * @return int|null + */ + public function getMinDepth() + { + if (!is_int($this->minDepth) || $this->minDepth < 0) { + return 0; + } + + return $this->minDepth; + } + + /** + * Render invisible items? + * + * @param bool $renderInvisible + * @return AbstractHelper + */ + public function setRenderInvisible($renderInvisible = true) + { + $this->renderInvisible = (bool) $renderInvisible; + return $this; + } + + /** + * Return renderInvisible flag + * + * @return bool + */ + public function getRenderInvisible() + { + return $this->renderInvisible; + } + + /** + * Sets ACL role(s) to use when iterating pages + * + * Implements {@link HelperInterface::setRole()}. + * + * @param mixed $role [optional] role to set. Expects a string, an + * instance of type {@link Acl\Role\RoleInterface}, or null. Default + * is null, which will set no role. + * @return AbstractHelper + * @throws Exception\InvalidArgumentException + */ + public function setRole($role = null) + { + if (null === $role || is_string($role) || + $role instanceof Acl\Role\RoleInterface + ) { + $this->role = $role; + } else { + throw new Exception\InvalidArgumentException(sprintf( + '$role must be a string, null, or an instance of ' + . 'Zend\Permissions\Role\RoleInterface; %s given', + (is_object($role) ? get_class($role) : gettype($role)) + )); + } + + return $this; + } + + /** + * Returns ACL role to use when iterating pages, or null if it isn't set + * using {@link setRole()} or {@link setDefaultRole()} + * + * Implements {@link HelperInterface::getRole()}. + * + * @return string|Acl\Role\RoleInterface|null + */ + public function getRole() + { + if ($this->role === null && static::$defaultRole !== null) { + return static::$defaultRole; + } + + return $this->role; + } + + /** + * Checks if the helper has an ACL role + * + * Implements {@link HelperInterface::hasRole()}. + * + * @return bool + */ + public function hasRole() + { + if ($this->role instanceof Acl\Role\RoleInterface + || is_string($this->role) + || static::$defaultRole instanceof Acl\Role\RoleInterface + || is_string(static::$defaultRole) + ) { + return true; + } + + return false; + } + + /** + * Set the service locator. + * + * @param ServiceLocatorInterface $serviceLocator + * @return AbstractHelper + */ + public function setServiceLocator(ServiceLocatorInterface $serviceLocator) + { + $this->serviceLocator = $serviceLocator; + return $this; + } + + /** + * Get the service locator. + * + * @return ServiceLocatorInterface + */ + public function getServiceLocator() + { + return $this->serviceLocator; + } + + // Translator methods - Good candidate to refactor as a trait with PHP 5.4 + + /** + * Sets translator to use in helper + * + * @param Translator $translator [optional] translator. + * Default is null, which sets no translator. + * @param string $textDomain [optional] text domain + * Default is null, which skips setTranslatorTextDomain + * @return AbstractHelper + */ + public function setTranslator(Translator $translator = null, $textDomain = null) + { + $this->translator = $translator; + if (null !== $textDomain) { + $this->setTranslatorTextDomain($textDomain); + } + + return $this; + } + + /** + * Returns translator used in helper + * + * @return Translator|null + */ + public function getTranslator() + { + if (! $this->isTranslatorEnabled()) { + return null; + } + + return $this->translator; + } + + /** + * Checks if the helper has a translator + * + * @return bool + */ + public function hasTranslator() + { + return (bool) $this->getTranslator(); + } + + /** + * Sets whether translator is enabled and should be used + * + * @param bool $enabled + * @return AbstractHelper + */ + public function setTranslatorEnabled($enabled = true) + { + $this->translatorEnabled = (bool) $enabled; + return $this; + } + + /** + * Returns whether translator is enabled and should be used + * + * @return bool + */ + public function isTranslatorEnabled() + { + return $this->translatorEnabled; + } + + /** + * Set translation text domain + * + * @param string $textDomain + * @return AbstractHelper + */ + public function setTranslatorTextDomain($textDomain = 'default') + { + $this->translatorTextDomain = $textDomain; + return $this; + } + + /** + * Return the translation text domain + * + * @return string + */ + public function getTranslatorTextDomain() + { + return $this->translatorTextDomain; + } + + /** + * Sets whether ACL should be used + * + * Implements {@link HelperInterface::setUseAcl()}. + * + * @param bool $useAcl + * @return AbstractHelper + */ + public function setUseAcl($useAcl = true) + { + $this->useAcl = (bool) $useAcl; + return $this; + } + + /** + * Returns whether ACL should be used + * + * Implements {@link HelperInterface::getUseAcl()}. + * + * @return bool + */ + public function getUseAcl() + { + return $this->useAcl; + } + + // Static methods: + + /** + * Sets default ACL to use if another ACL is not explicitly set + * + * @param Acl\AclInterface $acl [optional] ACL object. Default is null, which + * sets no ACL object. + * @return void + */ + public static function setDefaultAcl(Acl\AclInterface $acl = null) + { + static::$defaultAcl = $acl; + } + + /** + * Sets default ACL role(s) to use when iterating pages if not explicitly + * set later with {@link setRole()} + * + * @param mixed $role [optional] role to set. Expects null, string, or an + * instance of {@link Acl\Role\RoleInterface}. Default is null, which + * sets no default role. + * @return void + * @throws Exception\InvalidArgumentException if role is invalid + */ + public static function setDefaultRole($role = null) + { + if (null === $role + || is_string($role) + || $role instanceof Acl\Role\RoleInterface + ) { + static::$defaultRole = $role; + } else { + throw new Exception\InvalidArgumentException(sprintf( + '$role must be null|string|Zend\Permissions\Role\RoleInterface; received "%s"', + (is_object($role) ? get_class($role) : gettype($role)) + )); + } + } + + /** + * Attaches default ACL listeners, if ACLs are in use + */ + protected function setDefaultListeners() + { + if (!$this->getUseAcl()) { + return; + } + + $this->getEventManager()->getSharedManager()->attach( + 'Zend\View\Helper\Navigation\AbstractHelper', + 'isAllowed', + array('Zend\View\Helper\Navigation\Listener\AclListener', 'accept') + ); + } +} diff --git a/library/Zend/View/Helper/Navigation/Breadcrumbs.php b/library/Zend/View/Helper/Navigation/Breadcrumbs.php new file mode 100755 index 0000000000..5337ca8e07 --- /dev/null +++ b/library/Zend/View/Helper/Navigation/Breadcrumbs.php @@ -0,0 +1,292 @@ +setContainer($container); + } + + return $this; + } + + /** + * Renders helper + * + * Implements {@link HelperInterface::render()}. + * + * @param AbstractContainer $container [optional] container to render. Default is + * to render the container registered in the helper. + * @return string + */ + public function render($container = null) + { + $partial = $this->getPartial(); + if ($partial) { + return $this->renderPartial($container, $partial); + } + + return $this->renderStraight($container); + } + + /** + * Renders breadcrumbs by chaining 'a' elements with the separator + * registered in the helper + * + * @param AbstractContainer $container [optional] container to render. Default is + * to render the container registered in the helper. + * @return string + */ + public function renderStraight($container = null) + { + $this->parseContainer($container); + if (null === $container) { + $container = $this->getContainer(); + } + + // find deepest active + if (!$active = $this->findActive($container)) { + return ''; + } + + $active = $active['page']; + + // put the deepest active page last in breadcrumbs + if ($this->getLinkLast()) { + $html = $this->htmlify($active); + } else { + /** @var \Zend\View\Helper\EscapeHtml $escaper */ + $escaper = $this->view->plugin('escapeHtml'); + $html = $escaper( + $this->translate($active->getLabel(), $active->getTextDomain()) + ); + } + + // walk back to root + while ($parent = $active->getParent()) { + if ($parent instanceof AbstractPage) { + // prepend crumb to html + $html = $this->htmlify($parent) + . $this->getSeparator() + . $html; + } + + if ($parent === $container) { + // at the root of the given container + break; + } + + $active = $parent; + } + + return strlen($html) ? $this->getIndent() . $html : ''; + } + + /** + * Renders the given $container by invoking the partial view helper + * + * The container will simply be passed on as a model to the view script, + * so in the script it will be available in $this->container. + * + * @param AbstractContainer $container [optional] container to pass to view script. + * Default is to use the container registered + * in the helper. + * @param string|array $partial [optional] partial view script to use. + * Default is to use the partial registered + * in the helper. If an array is given, it + * is expected to contain two values; the + * partial view script to use, and the module + * where the script can be found. + * @throws Exception\RuntimeException if no partial provided + * @throws Exception\InvalidArgumentException if partial is invalid array + * @return string helper output + */ + public function renderPartial($container = null, $partial = null) + { + $this->parseContainer($container); + if (null === $container) { + $container = $this->getContainer(); + } + + if (null === $partial) { + $partial = $this->getPartial(); + } + + if (empty($partial)) { + throw new Exception\RuntimeException( + 'Unable to render menu: No partial view script provided' + ); + } + + // put breadcrumb pages in model + $model = array( + 'pages' => array(), + 'separator' => $this->getSeparator() + ); + $active = $this->findActive($container); + if ($active) { + $active = $active['page']; + $model['pages'][] = $active; + while ($parent = $active->getParent()) { + if ($parent instanceof AbstractPage) { + $model['pages'][] = $parent; + } else { + break; + } + + if ($parent === $container) { + // break if at the root of the given container + break; + } + + $active = $parent; + } + $model['pages'] = array_reverse($model['pages']); + } + + /** @var \Zend\View\Helper\Partial $partialHelper */ + $partialHelper = $this->view->plugin('partial'); + + if (is_array($partial)) { + if (count($partial) != 2) { + throw new Exception\InvalidArgumentException( + 'Unable to render menu: A view partial supplied as ' + . 'an array must contain two values: partial view ' + . 'script and module where script can be found' + ); + } + + return $partialHelper($partial[0], $model); + } + + return $partialHelper($partial, $model); + } + + /** + * Sets whether last page in breadcrumbs should be hyperlinked + * + * @param bool $linkLast whether last page should be hyperlinked + * @return Breadcrumbs + */ + public function setLinkLast($linkLast) + { + $this->linkLast = (bool) $linkLast; + return $this; + } + + /** + * Returns whether last page in breadcrumbs should be hyperlinked + * + * @return bool + */ + public function getLinkLast() + { + return $this->linkLast; + } + + /** + * Sets which partial view script to use for rendering menu + * + * @param string|array $partial partial view script or null. If an array is + * given, it is expected to contain two + * values; the partial view script to use, + * and the module where the script can be + * found. + * @return Breadcrumbs + */ + public function setPartial($partial) + { + if (null === $partial || is_string($partial) || is_array($partial)) { + $this->partial = $partial; + } + + return $this; + } + + /** + * Returns partial view script to use for rendering menu + * + * @return string|array|null + */ + public function getPartial() + { + return $this->partial; + } + + /** + * Sets breadcrumb separator + * + * @param string $separator separator string + * @return Breadcrumbs + */ + public function setSeparator($separator) + { + if (is_string($separator)) { + $this->separator = $separator; + } + + return $this; + } + + /** + * Returns breadcrumb separator + * + * @return string breadcrumb separator + */ + public function getSeparator() + { + return $this->separator; + } +} diff --git a/library/Zend/View/Helper/Navigation/HelperInterface.php b/library/Zend/View/Helper/Navigation/HelperInterface.php new file mode 100755 index 0000000000..8b52c8c1da --- /dev/null +++ b/library/Zend/View/Helper/Navigation/HelperInterface.php @@ -0,0 +1,143 @@ + elements + */ +class Links extends AbstractHelper +{ + /** + * Constants used for specifying which link types to find and render + * + * @var int + */ + const RENDER_ALTERNATE = 0x0001; + const RENDER_STYLESHEET = 0x0002; + const RENDER_START = 0x0004; + const RENDER_NEXT = 0x0008; + const RENDER_PREV = 0x0010; + const RENDER_CONTENTS = 0x0020; + const RENDER_INDEX = 0x0040; + const RENDER_GLOSSARY = 0x0080; + const RENDER_COPYRIGHT = 0x0100; + const RENDER_CHAPTER = 0x0200; + const RENDER_SECTION = 0x0400; + const RENDER_SUBSECTION = 0x0800; + const RENDER_APPENDIX = 0x1000; + const RENDER_HELP = 0x2000; + const RENDER_BOOKMARK = 0x4000; + const RENDER_CUSTOM = 0x8000; + const RENDER_ALL = 0xffff; + + /** + * Maps render constants to W3C link types + * + * @var array + */ + protected static $RELATIONS = array( + self::RENDER_ALTERNATE => 'alternate', + self::RENDER_STYLESHEET => 'stylesheet', + self::RENDER_START => 'start', + self::RENDER_NEXT => 'next', + self::RENDER_PREV => 'prev', + self::RENDER_CONTENTS => 'contents', + self::RENDER_INDEX => 'index', + self::RENDER_GLOSSARY => 'glossary', + self::RENDER_COPYRIGHT => 'copyright', + self::RENDER_CHAPTER => 'chapter', + self::RENDER_SECTION => 'section', + self::RENDER_SUBSECTION => 'subsection', + self::RENDER_APPENDIX => 'appendix', + self::RENDER_HELP => 'help', + self::RENDER_BOOKMARK => 'bookmark', + ); + + /** + * The helper's render flag + * + * @see render() + * @see setRenderFlag() + * @var int + */ + protected $renderFlag = self::RENDER_ALL; + + /** + * Root container + * + * Used for preventing methods to traverse above the container given to + * the {@link render()} method. + * + * @see _findRoot() + * @var AbstractContainer + */ + protected $root; + + /** + * Helper entry point + * + * @param string|AbstractContainer $container container to operate on + * @return Links + */ + public function __invoke($container = null) + { + if (null !== $container) { + $this->setContainer($container); + } + + return $this; + } + + /** + * Magic overload: Proxy calls to {@link findRelation()} or container + * + * Examples of finder calls: + * + * // METHOD // SAME AS + * $h->findRelNext($page); // $h->findRelation($page, 'rel', 'next') + * $h->findRevSection($page); // $h->findRelation($page, 'rev', 'section'); + * $h->findRelFoo($page); // $h->findRelation($page, 'rel', 'foo'); + * + * + * @param string $method + * @param array $arguments + * @return mixed + * @throws Exception\ExceptionInterface + */ + public function __call($method, array $arguments = array()) + { + ErrorHandler::start(E_WARNING); + $result = preg_match('/find(Rel|Rev)(.+)/', $method, $match); + ErrorHandler::stop(); + if ($result) { + return $this->findRelation($arguments[0], + strtolower($match[1]), + strtolower($match[2])); + } + + return parent::__call($method, $arguments); + } + + /** + * Renders helper + * + * Implements {@link HelperInterface::render()}. + * + * @param AbstractContainer|string|null $container [optional] container to render. + * Default is to render the + * container registered in the + * helper. + * @return string + */ + public function render($container = null) + { + $this->parseContainer($container); + if (null === $container) { + $container = $this->getContainer(); + } + + $active = $this->findActive($container); + if ($active) { + $active = $active['page']; + } else { + // no active page + return ''; + } + + $output = ''; + $indent = $this->getIndent(); + $this->root = $container; + + $result = $this->findAllRelations($active, $this->getRenderFlag()); + foreach ($result as $attrib => $types) { + foreach ($types as $relation => $pages) { + foreach ($pages as $page) { + $r = $this->renderLink($page, $attrib, $relation); + if ($r) { + $output .= $indent . $r . self::EOL; + } + } + } + } + + $this->root = null; + + // return output (trim last newline by spec) + return strlen($output) ? rtrim($output, self::EOL) : ''; + } + + /** + * Renders the given $page as a link element, with $attrib = $relation + * + * @param AbstractPage $page the page to render the link for + * @param string $attrib the attribute to use for $type, + * either 'rel' or 'rev' + * @param string $relation relation type, muse be one of; + * alternate, appendix, bookmark, + * chapter, contents, copyright, + * glossary, help, home, index, next, + * prev, section, start, stylesheet, + * subsection + * @return string + * @throws Exception\DomainException + */ + public function renderLink(AbstractPage $page, $attrib, $relation) + { + if (!in_array($attrib, array('rel', 'rev'))) { + throw new Exception\DomainException(sprintf( + 'Invalid relation attribute "%s", must be "rel" or "rev"', + $attrib + )); + } + + if (!$href = $page->getHref()) { + return ''; + } + + // TODO: add more attribs + // http://www.w3.org/TR/html401/struct/links.html#h-12.2 + $attribs = array( + $attrib => $relation, + 'href' => $href, + 'title' => $page->getLabel() + ); + + return 'htmlAttribs($attribs) . + $this->getClosingBracket(); + } + + // Finder methods: + + /** + * Finds all relations (forward and reverse) for the given $page + * + * The form of the returned array: + * + * // $page denotes an instance of Zend\Navigation\Page\AbstractPage + * $returned = array( + * 'rel' => array( + * 'alternate' => array($page, $page, $page), + * 'start' => array($page), + * 'next' => array($page), + * 'prev' => array($page), + * 'canonical' => array($page) + * ), + * 'rev' => array( + * 'section' => array($page) + * ) + * ); + * + * + * @param AbstractPage $page page to find links for + * @param null|int + * @return array + */ + public function findAllRelations(AbstractPage $page, $flag = null) + { + if (!is_int($flag)) { + $flag = self::RENDER_ALL; + } + + $result = array('rel' => array(), 'rev' => array()); + $native = array_values(static::$RELATIONS); + + foreach (array_keys($result) as $rel) { + $meth = 'getDefined' . ucfirst($rel); + $types = array_merge($native, array_diff($page->$meth(), $native)); + + foreach ($types as $type) { + if (!$relFlag = array_search($type, static::$RELATIONS)) { + $relFlag = self::RENDER_CUSTOM; + } + if (!($flag & $relFlag)) { + continue; + } + + $found = $this->findRelation($page, $rel, $type); + if ($found) { + if (!is_array($found)) { + $found = array($found); + } + $result[$rel][$type] = $found; + } + } + } + + return $result; + } + + /** + * Finds relations of the given $rel=$type from $page + * + * This method will first look for relations in the page instance, then + * by searching the root container if nothing was found in the page. + * + * @param AbstractPage $page page to find relations for + * @param string $rel relation, "rel" or "rev" + * @param string $type link type, e.g. 'start', 'next' + * @return AbstractPage|array|null + * @throws Exception\DomainException if $rel is not "rel" or "rev" + */ + public function findRelation(AbstractPage $page, $rel, $type) + { + if (!in_array($rel, array('rel', 'rev'))) { + throw new Exception\DomainException(sprintf( + 'Invalid argument: $rel must be "rel" or "rev"; "%s" given', + $rel + )); + } + + if (!$result = $this->findFromProperty($page, $rel, $type)) { + $result = $this->findFromSearch($page, $rel, $type); + } + + return $result; + } + + /** + * Finds relations of given $type for $page by checking if the + * relation is specified as a property of $page + * + * @param AbstractPage $page page to find relations for + * @param string $rel relation, 'rel' or 'rev' + * @param string $type link type, e.g. 'start', 'next' + * @return AbstractPage|array|null + */ + protected function findFromProperty(AbstractPage $page, $rel, $type) + { + $method = 'get' . ucfirst($rel); + $result = $page->$method($type); + if ($result) { + $result = $this->convertToPages($result); + if ($result) { + if (!is_array($result)) { + $result = array($result); + } + + foreach ($result as $key => $page) { + if (!$this->accept($page)) { + unset($result[$key]); + } + } + + return count($result) == 1 ? $result[0] : $result; + } + } + + return null; + } + + /** + * Finds relations of given $rel=$type for $page by using the helper to + * search for the relation in the root container + * + * @param AbstractPage $page page to find relations for + * @param string $rel relation, 'rel' or 'rev' + * @param string $type link type, e.g. 'start', 'next', etc + * @return array|null + */ + protected function findFromSearch(AbstractPage $page, $rel, $type) + { + $found = null; + + $method = 'search' . ucfirst($rel) . ucfirst($type); + if (method_exists($this, $method)) { + $found = $this->$method($page); + } + + return $found; + } + + // Search methods: + + /** + * Searches the root container for the forward 'start' relation of the given + * $page + * + * From {@link http://www.w3.org/TR/html4/types.html#type-links}: + * Refers to the first document in a collection of documents. This link type + * tells search engines which document is considered by the author to be the + * starting point of the collection. + * + * @param AbstractPage $page + * @return AbstractPage|null + */ + public function searchRelStart(AbstractPage $page) + { + $found = $this->findRoot($page); + if (!$found instanceof AbstractPage) { + $found->rewind(); + $found = $found->current(); + } + + if ($found === $page || !$this->accept($found)) { + $found = null; + } + + return $found; + } + + /** + * Searches the root container for the forward 'next' relation of the given + * $page + * + * From {@link http://www.w3.org/TR/html4/types.html#type-links}: + * Refers to the next document in a linear sequence of documents. User + * agents may choose to preload the "next" document, to reduce the perceived + * load time. + * + * @param AbstractPage $page + * @return AbstractPage|null + */ + public function searchRelNext(AbstractPage $page) + { + $found = null; + $break = false; + $iterator = new RecursiveIteratorIterator($this->findRoot($page), + RecursiveIteratorIterator::SELF_FIRST); + foreach ($iterator as $intermediate) { + if ($intermediate === $page) { + // current page; break at next accepted page + $break = true; + continue; + } + + if ($break && $this->accept($intermediate)) { + $found = $intermediate; + break; + } + } + + return $found; + } + + /** + * Searches the root container for the forward 'prev' relation of the given + * $page + * + * From {@link http://www.w3.org/TR/html4/types.html#type-links}: + * Refers to the previous document in an ordered series of documents. Some + * user agents also support the synonym "Previous". + * + * @param AbstractPage $page + * @return AbstractPage|null + */ + public function searchRelPrev(AbstractPage $page) + { + $found = null; + $prev = null; + $iterator = new RecursiveIteratorIterator( + $this->findRoot($page), + RecursiveIteratorIterator::SELF_FIRST); + foreach ($iterator as $intermediate) { + if (!$this->accept($intermediate)) { + continue; + } + if ($intermediate === $page) { + $found = $prev; + break; + } + + $prev = $intermediate; + } + + return $found; + } + + /** + * Searches the root container for forward 'chapter' relations of the given + * $page + * + * From {@link http://www.w3.org/TR/html4/types.html#type-links}: + * Refers to a document serving as a chapter in a collection of documents. + * + * @param AbstractPage $page + * @return AbstractPage|array|null + */ + public function searchRelChapter(AbstractPage $page) + { + $found = array(); + + // find first level of pages + $root = $this->findRoot($page); + + // find start page(s) + $start = $this->findRelation($page, 'rel', 'start'); + if (!is_array($start)) { + $start = array($start); + } + + foreach ($root as $chapter) { + // exclude self and start page from chapters + if ($chapter !== $page && + !in_array($chapter, $start) && + $this->accept($chapter)) { + $found[] = $chapter; + } + } + + switch (count($found)) { + case 0: + return null; + case 1: + return $found[0]; + default: + return $found; + } + } + + /** + * Searches the root container for forward 'section' relations of the given + * $page + * + * From {@link http://www.w3.org/TR/html4/types.html#type-links}: + * Refers to a document serving as a section in a collection of documents. + * + * @param AbstractPage $page + * @return AbstractPage|array|null + */ + public function searchRelSection(AbstractPage $page) + { + $found = array(); + + // check if given page has pages and is a chapter page + if ($page->hasPages() && $this->findRoot($page)->hasPage($page)) { + foreach ($page as $section) { + if ($this->accept($section)) { + $found[] = $section; + } + } + } + + switch (count($found)) { + case 0: + return null; + case 1: + return $found[0]; + default: + return $found; + } + } + + /** + * Searches the root container for forward 'subsection' relations of the + * given $page + * + * From {@link http://www.w3.org/TR/html4/types.html#type-links}: + * Refers to a document serving as a subsection in a collection of + * documents. + * + * @param AbstractPage $page + * @return AbstractPage|array|null + */ + public function searchRelSubsection(AbstractPage $page) + { + $found = array(); + + if ($page->hasPages()) { + // given page has child pages, loop chapters + foreach ($this->findRoot($page) as $chapter) { + // is page a section? + if ($chapter->hasPage($page)) { + foreach ($page as $subsection) { + if ($this->accept($subsection)) { + $found[] = $subsection; + } + } + } + } + } + + switch (count($found)) { + case 0: + return null; + case 1: + return $found[0]; + default: + return $found; + } + } + + /** + * Searches the root container for the reverse 'section' relation of the + * given $page + * + * From {@link http://www.w3.org/TR/html4/types.html#type-links}: + * Refers to a document serving as a section in a collection of documents. + * + * @param AbstractPage $page + * @return AbstractPage|null + */ + public function searchRevSection(AbstractPage $page) + { + $found = null; + $parent = $page->getParent(); + if ($parent) { + if ($parent instanceof AbstractPage && + $this->findRoot($page)->hasPage($parent)) { + $found = $parent; + } + } + + return $found; + } + + /** + * Searches the root container for the reverse 'section' relation of the + * given $page + * + * From {@link http://www.w3.org/TR/html4/types.html#type-links}: + * Refers to a document serving as a subsection in a collection of + * documents. + * + * @param AbstractPage $page + * @return AbstractPage|null + */ + public function searchRevSubsection(AbstractPage $page) + { + $found = null; + $parent = $page->getParent(); + if ($parent) { + if ($parent instanceof AbstractPage) { + $root = $this->findRoot($page); + foreach ($root as $chapter) { + if ($chapter->hasPage($parent)) { + $found = $parent; + break; + } + } + } + } + + return $found; + } + + // Util methods: + + /** + * Returns the root container of the given page + * + * When rendering a container, the render method still store the given + * container as the root container, and unset it when done rendering. This + * makes sure finder methods will not traverse above the container given + * to the render method. + * + * @param AbstractPage $page + * @return AbstractContainer + */ + protected function findRoot(AbstractPage $page) + { + if ($this->root) { + return $this->root; + } + + $root = $page; + + while ($parent = $page->getParent()) { + $root = $parent; + if ($parent instanceof AbstractPage) { + $page = $parent; + } else { + break; + } + } + + return $root; + } + + /** + * Converts a $mixed value to an array of pages + * + * @param mixed $mixed mixed value to get page(s) from + * @param bool $recursive whether $value should be looped + * if it is an array or a config + * @return AbstractPage|array|null + */ + protected function convertToPages($mixed, $recursive = true) + { + if ($mixed instanceof AbstractPage) { + // value is a page instance; return directly + return $mixed; + } elseif ($mixed instanceof AbstractContainer) { + // value is a container; return pages in it + $pages = array(); + foreach ($mixed as $page) { + $pages[] = $page; + } + return $pages; + } elseif ($mixed instanceof Traversable) { + $mixed = ArrayUtils::iteratorToArray($mixed); + } elseif (is_string($mixed)) { + // value is a string; make a URI page + return AbstractPage::factory(array( + 'type' => 'uri', + 'uri' => $mixed + )); + } + + if (is_array($mixed) && !empty($mixed)) { + if ($recursive && is_numeric(key($mixed))) { + // first key is numeric; assume several pages + $pages = array(); + foreach ($mixed as $value) { + $value = $this->convertToPages($value, false); + if ($value) { + $pages[] = $value; + } + } + return $pages; + } else { + // pass array to factory directly + try { + $page = AbstractPage::factory($mixed); + return $page; + } catch (\Exception $e) { + } + } + } + + // nothing found + return null; + } + + /** + * Sets the helper's render flag + * + * The helper uses the bitwise '&' operator against the hex values of the + * render constants. This means that the flag can is "bitwised" value of + * the render constants. Examples: + * + * // render all links except glossary + * $flag = Links:RENDER_ALL ^ Links:RENDER_GLOSSARY; + * $helper->setRenderFlag($flag); + * + * // render only chapters and sections + * $flag = Links:RENDER_CHAPTER | Links:RENDER_SECTION; + * $helper->setRenderFlag($flag); + * + * // render only relations that are not native W3C relations + * $helper->setRenderFlag(Links:RENDER_CUSTOM); + * + * // render all relations (default) + * $helper->setRenderFlag(Links:RENDER_ALL); + * + * + * Note that custom relations can also be rendered directly using the + * {@link renderLink()} method. + * + * @param int $renderFlag + * @return Links + */ + public function setRenderFlag($renderFlag) + { + $this->renderFlag = (int) $renderFlag; + + return $this; + } + + /** + * Returns the helper's render flag + * + * @return int + */ + public function getRenderFlag() + { + return $this->renderFlag; + } +} diff --git a/library/Zend/View/Helper/Navigation/Listener/AclListener.php b/library/Zend/View/Helper/Navigation/Listener/AclListener.php new file mode 100755 index 0000000000..5c8656084e --- /dev/null +++ b/library/Zend/View/Helper/Navigation/Listener/AclListener.php @@ -0,0 +1,55 @@ +getParams(); + $acl = $params['acl']; + $page = $params['page']; + $role = $params['role']; + + if (!$acl) { + return $accepted; + } + + $resource = $page->getResource(); + $privilege = $page->getPrivilege(); + + if ($resource || $privilege) { + $accepted = $acl->hasResource($resource) + && $acl->isAllowed($role, $resource, $privilege); + } + + return $accepted; + } +} diff --git a/library/Zend/View/Helper/Navigation/Menu.php b/library/Zend/View/Helper/Navigation/Menu.php new file mode 100755 index 0000000000..29d7199234 --- /dev/null +++ b/library/Zend/View/Helper/Navigation/Menu.php @@ -0,0 +1,765 @@ + element + * + * @var bool + */ + protected $addClassToListItem = false; + + /** + * Whether labels should be escaped + * + * @var bool + */ + protected $escapeLabels = true; + + /** + * Whether only active branch should be rendered + * + * @var bool + */ + protected $onlyActiveBranch = false; + + /** + * Partial view script to use for rendering menu + * + * @var string|array + */ + protected $partial = null; + + /** + * Whether parents should be rendered when only rendering active branch + * + * @var bool + */ + protected $renderParents = true; + + /** + * CSS class to use for the ul element + * + * @var string + */ + protected $ulClass = 'navigation'; + + /** + * CSS class to use for the active li element + * + * @var string + */ + protected $liActiveClass = 'active'; + + /** + * View helper entry point: + * Retrieves helper and optionally sets container to operate on + * + * @param AbstractContainer $container [optional] container to operate on + * @return self + */ + public function __invoke($container = null) + { + if (null !== $container) { + $this->setContainer($container); + } + + return $this; + } + + /** + * Renders menu + * + * Implements {@link HelperInterface::render()}. + * + * If a partial view is registered in the helper, the menu will be rendered + * using the given partial script. If no partial is registered, the menu + * will be rendered as an 'ul' element by the helper's internal method. + * + * @see renderPartial() + * @see renderMenu() + * + * @param AbstractContainer $container [optional] container to render. Default is + * to render the container registered in the helper. + * @return string + */ + public function render($container = null) + { + $partial = $this->getPartial(); + if ($partial) { + return $this->renderPartial($container, $partial); + } + + return $this->renderMenu($container); + } + + /** + * Renders the deepest active menu within [$minDepth, $maxDepth], (called + * from {@link renderMenu()}) + * + * @param AbstractContainer $container container to render + * @param string $ulClass CSS class for first UL + * @param string $indent initial indentation + * @param int|null $minDepth minimum depth + * @param int|null $maxDepth maximum depth + * @param bool $escapeLabels Whether or not to escape the labels + * @param bool $addClassToListItem Whether or not page class applied to
  • element + * @param string $liActiveClass CSS class for active LI + * @return string + */ + protected function renderDeepestMenu( + AbstractContainer $container, + $ulClass, + $indent, + $minDepth, + $maxDepth, + $escapeLabels, + $addClassToListItem, + $liActiveClass + ) { + if (!$active = $this->findActive($container, $minDepth - 1, $maxDepth)) { + return ''; + } + + // special case if active page is one below minDepth + if ($active['depth'] < $minDepth) { + if (!$active['page']->hasPages(!$this->renderInvisible)) { + return ''; + } + } elseif (!$active['page']->hasPages(!$this->renderInvisible)) { + // found pages has no children; render siblings + $active['page'] = $active['page']->getParent(); + } elseif (is_int($maxDepth) && $active['depth'] +1 > $maxDepth) { + // children are below max depth; render siblings + $active['page'] = $active['page']->getParent(); + } + + /* @var $escaper \Zend\View\Helper\EscapeHtmlAttr */ + $escaper = $this->view->plugin('escapeHtmlAttr'); + $ulClass = $ulClass ? ' class="' . $escaper($ulClass) . '"' : ''; + $html = $indent . '' . PHP_EOL; + + foreach ($active['page'] as $subPage) { + if (!$this->accept($subPage)) { + continue; + } + + // render li tag and page + $liClasses = array(); + // Is page active? + if ($subPage->isActive(true)) { + $liClasses[] = $liActiveClass; + } + // Add CSS class from page to
  • + if ($addClassToListItem && $subPage->getClass()) { + $liClasses[] = $subPage->getClass(); + } + $liClass = empty($liClasses) ? '' : ' class="' . $escaper(implode(' ', $liClasses)) . '"'; + + $html .= $indent . ' ' . PHP_EOL; + $html .= $indent . ' ' . $this->htmlify($subPage, $escapeLabels, $addClassToListItem) . PHP_EOL; + $html .= $indent . '
  • ' . PHP_EOL; + } + + $html .= $indent . ''; + + return $html; + } + + /** + * Renders helper + * + * Renders a HTML 'ul' for the given $container. If $container is not given, + * the container registered in the helper will be used. + * + * Available $options: + * + * + * @param AbstractContainer $container [optional] container to create menu from. + * Default is to use the container retrieved + * from {@link getContainer()}. + * @param array $options [optional] options for controlling rendering + * @return string + */ + public function renderMenu($container = null, array $options = array()) + { + $this->parseContainer($container); + if (null === $container) { + $container = $this->getContainer(); + } + + + $options = $this->normalizeOptions($options); + + if ($options['onlyActiveBranch'] && !$options['renderParents']) { + $html = $this->renderDeepestMenu($container, + $options['ulClass'], + $options['indent'], + $options['minDepth'], + $options['maxDepth'], + $options['escapeLabels'], + $options['addClassToListItem'], + $options['liActiveClass'] + ); + } else { + $html = $this->renderNormalMenu($container, + $options['ulClass'], + $options['indent'], + $options['minDepth'], + $options['maxDepth'], + $options['onlyActiveBranch'], + $options['escapeLabels'], + $options['addClassToListItem'], + $options['liActiveClass'] + ); + } + + return $html; + } + + /** + * Renders a normal menu (called from {@link renderMenu()}) + * + * @param AbstractContainer $container container to render + * @param string $ulClass CSS class for first UL + * @param string $indent initial indentation + * @param int|null $minDepth minimum depth + * @param int|null $maxDepth maximum depth + * @param bool $onlyActive render only active branch? + * @param bool $escapeLabels Whether or not to escape the labels + * @param bool $addClassToListItem Whether or not page class applied to
  • element + * @param string $liActiveClass CSS class for active LI + * @return string + */ + protected function renderNormalMenu( + AbstractContainer $container, + $ulClass, + $indent, + $minDepth, + $maxDepth, + $onlyActive, + $escapeLabels, + $addClassToListItem, + $liActiveClass + ) { + $html = ''; + + // find deepest active + $found = $this->findActive($container, $minDepth, $maxDepth); + /* @var $escaper \Zend\View\Helper\EscapeHtmlAttr */ + $escaper = $this->view->plugin('escapeHtmlAttr'); + + if ($found) { + $foundPage = $found['page']; + $foundDepth = $found['depth']; + } else { + $foundPage = null; + } + + // create iterator + $iterator = new RecursiveIteratorIterator($container, + RecursiveIteratorIterator::SELF_FIRST); + if (is_int($maxDepth)) { + $iterator->setMaxDepth($maxDepth); + } + + // iterate container + $prevDepth = -1; + foreach ($iterator as $page) { + $depth = $iterator->getDepth(); + $isActive = $page->isActive(true); + if ($depth < $minDepth || !$this->accept($page)) { + // page is below minDepth or not accepted by acl/visibility + continue; + } elseif ($onlyActive && !$isActive) { + // page is not active itself, but might be in the active branch + $accept = false; + if ($foundPage) { + if ($foundPage->hasPage($page)) { + // accept if page is a direct child of the active page + $accept = true; + } elseif ($foundPage->getParent()->hasPage($page)) { + // page is a sibling of the active page... + if (!$foundPage->hasPages(!$this->renderInvisible) || + is_int($maxDepth) && $foundDepth + 1 > $maxDepth) { + // accept if active page has no children, or the + // children are too deep to be rendered + $accept = true; + } + } + } + + if (!$accept) { + continue; + } + } + + // make sure indentation is correct + $depth -= $minDepth; + $myIndent = $indent . str_repeat(' ', $depth); + + if ($depth > $prevDepth) { + // start new ul tag + if ($ulClass && $depth == 0) { + $ulClass = ' class="' . $escaper($ulClass) . '"'; + } else { + $ulClass = ''; + } + $html .= $myIndent . '' . PHP_EOL; + } elseif ($prevDepth > $depth) { + // close li/ul tags until we're at current depth + for ($i = $prevDepth; $i > $depth; $i--) { + $ind = $indent . str_repeat(' ', $i); + $html .= $ind . '
  • ' . PHP_EOL; + $html .= $ind . '' . PHP_EOL; + } + // close previous li tag + $html .= $myIndent . ' ' . PHP_EOL; + } else { + // close previous li tag + $html .= $myIndent . ' ' . PHP_EOL; + } + + // render li tag and page + $liClasses = array(); + // Is page active? + if ($isActive) { + $liClasses[] = $liActiveClass; + } + // Add CSS class from page to
  • + if ($addClassToListItem && $page->getClass()) { + $liClasses[] = $page->getClass(); + } + $liClass = empty($liClasses) ? '' : ' class="' . $escaper(implode(' ', $liClasses)) . '"'; + + $html .= $myIndent . ' ' . PHP_EOL + . $myIndent . ' ' . $this->htmlify($page, $escapeLabels, $addClassToListItem) . PHP_EOL; + + // store as previous depth for next iteration + $prevDepth = $depth; + } + + if ($html) { + // done iterating container; close open ul/li tags + for ($i = $prevDepth+1; $i > 0; $i--) { + $myIndent = $indent . str_repeat(' ', $i-1); + $html .= $myIndent . '
  • ' . PHP_EOL + . $myIndent . '' . PHP_EOL; + } + $html = rtrim($html, PHP_EOL); + } + + return $html; + } + + /** + * Renders the given $container by invoking the partial view helper + * + * The container will simply be passed on as a model to the view script + * as-is, and will be available in the partial script as 'container', e.g. + * echo 'Number of pages: ', count($this->container);. + * + * @param AbstractContainer $container [optional] container to pass to view + * script. Default is to use the container + * registered in the helper. + * @param string|array $partial [optional] partial view script to use. + * Default is to use the partial + * registered in the helper. If an array + * is given, it is expected to contain two + * values; the partial view script to use, + * and the module where the script can be + * found. + * @return string + * @throws Exception\RuntimeException if no partial provided + * @throws Exception\InvalidArgumentException if partial is invalid array + */ + public function renderPartial($container = null, $partial = null) + { + $this->parseContainer($container); + if (null === $container) { + $container = $this->getContainer(); + } + + if (null === $partial) { + $partial = $this->getPartial(); + } + + if (empty($partial)) { + throw new Exception\RuntimeException( + 'Unable to render menu: No partial view script provided' + ); + } + + $model = array( + 'container' => $container + ); + + /** @var \Zend\View\Helper\Partial $partialHelper */ + $partialHelper = $this->view->plugin('partial'); + + if (is_array($partial)) { + if (count($partial) != 2) { + throw new Exception\InvalidArgumentException( + 'Unable to render menu: A view partial supplied as ' + . 'an array must contain two values: partial view ' + . 'script and module where script can be found' + ); + } + + return $partialHelper($partial[0], $model); + } + + return $partialHelper($partial, $model); + } + + /** + * Renders the inner-most sub menu for the active page in the $container + * + * This is a convenience method which is equivalent to the following call: + * + * renderMenu($container, array( + * 'indent' => $indent, + * 'ulClass' => $ulClass, + * 'minDepth' => null, + * 'maxDepth' => null, + * 'onlyActiveBranch' => true, + * 'renderParents' => false, + * 'liActiveClass' => $liActiveClass + * )); + * + * + * @param AbstractContainer $container [optional] container to + * render. Default is to render + * the container registered in + * the helper. + * @param string $ulClass [optional] CSS class to + * use for UL element. Default + * is to use the value from + * {@link getUlClass()}. + * @param string|int $indent [optional] indentation as + * a string or number of + * spaces. Default is to use + * the value retrieved from + * {@link getIndent()}. + * @param string $liActiveClass [optional] CSS class to + * use for UL element. Default + * is to use the value from + * {@link getUlClass()}. + * @return string + */ + public function renderSubMenu( + AbstractContainer $container = null, + $ulClass = null, + $indent = null, + $liActiveClass = null + ) { + return $this->renderMenu($container, array( + 'indent' => $indent, + 'ulClass' => $ulClass, + 'minDepth' => null, + 'maxDepth' => null, + 'onlyActiveBranch' => true, + 'renderParents' => false, + 'escapeLabels' => true, + 'addClassToListItem' => false, + 'liActiveClass' => $liActiveClass + )); + } + + /** + * Returns an HTML string containing an 'a' element for the given page if + * the page's href is not empty, and a 'span' element if it is empty + * + * Overrides {@link AbstractHelper::htmlify()}. + * + * @param AbstractPage $page page to generate HTML for + * @param bool $escapeLabel Whether or not to escape the label + * @param bool $addClassToListItem Whether or not to add the page class to the list item + * @return string + */ + public function htmlify(AbstractPage $page, $escapeLabel = true, $addClassToListItem = false) + { + // get attribs for element + $attribs = array( + 'id' => $page->getId(), + 'title' => $this->translate($page->getTitle(), $page->getTextDomain()), + ); + + if ($addClassToListItem === false) { + $attribs['class'] = $page->getClass(); + } + + // does page have a href? + $href = $page->getHref(); + if ($href) { + $element = 'a'; + $attribs['href'] = $href; + $attribs['target'] = $page->getTarget(); + } else { + $element = 'span'; + } + + $html = '<' . $element . $this->htmlAttribs($attribs) . '>'; + $label = $this->translate($page->getLabel(), $page->getTextDomain()); + if ($escapeLabel === true) { + /** @var \Zend\View\Helper\EscapeHtml $escaper */ + $escaper = $this->view->plugin('escapeHtml'); + $html .= $escaper($label); + } else { + $html .= $label; + } + $html .= ''; + + return $html; + } + + /** + * Normalizes given render options + * + * @param array $options [optional] options to normalize + * @return array + */ + protected function normalizeOptions(array $options = array()) + { + if (isset($options['indent'])) { + $options['indent'] = $this->getWhitespace($options['indent']); + } else { + $options['indent'] = $this->getIndent(); + } + + if (isset($options['ulClass']) && $options['ulClass'] !== null) { + $options['ulClass'] = (string) $options['ulClass']; + } else { + $options['ulClass'] = $this->getUlClass(); + } + + if (array_key_exists('minDepth', $options)) { + if (null !== $options['minDepth']) { + $options['minDepth'] = (int) $options['minDepth']; + } + } else { + $options['minDepth'] = $this->getMinDepth(); + } + + if ($options['minDepth'] < 0 || $options['minDepth'] === null) { + $options['minDepth'] = 0; + } + + if (array_key_exists('maxDepth', $options)) { + if (null !== $options['maxDepth']) { + $options['maxDepth'] = (int) $options['maxDepth']; + } + } else { + $options['maxDepth'] = $this->getMaxDepth(); + } + + if (!isset($options['onlyActiveBranch'])) { + $options['onlyActiveBranch'] = $this->getOnlyActiveBranch(); + } + + if (!isset($options['escapeLabels'])) { + $options['escapeLabels'] = $this->escapeLabels; + } + + if (!isset($options['renderParents'])) { + $options['renderParents'] = $this->getRenderParents(); + } + + if (!isset($options['addClassToListItem'])) { + $options['addClassToListItem'] = $this->getAddClassToListItem(); + } + + if (isset($options['liActiveClass']) && $options['liActiveClass'] !== null) { + $options['liActiveClass'] = (string) $options['liActiveClass']; + } else { + $options['liActiveClass'] = $this->getLiActiveClass(); + } + + return $options; + } + + /** + * Sets a flag indicating whether labels should be escaped + * + * @param bool $flag [optional] escape labels + * @return self + */ + public function escapeLabels($flag = true) + { + $this->escapeLabels = (bool) $flag; + return $this; + } + + /** + * Enables/disables page class applied to
  • element + * + * @param bool $flag [optional] page class applied to
  • element + * Default is true. + * @return self fluent interface, returns self + */ + public function setAddClassToListItem($flag = true) + { + $this->addClassToListItem = (bool) $flag; + return $this; + } + + /** + * Returns flag indicating whether page class should be applied to
  • element + * + * By default, this value is false. + * + * @return bool whether parents should be rendered + */ + public function getAddClassToListItem() + { + return $this->addClassToListItem; + } + + /** + * Sets a flag indicating whether only active branch should be rendered + * + * @param bool $flag [optional] render only active branch. + * @return self + */ + public function setOnlyActiveBranch($flag = true) + { + $this->onlyActiveBranch = (bool) $flag; + return $this; + } + + /** + * Returns a flag indicating whether only active branch should be rendered + * + * By default, this value is false, meaning the entire menu will be + * be rendered. + * + * @return bool + */ + public function getOnlyActiveBranch() + { + return $this->onlyActiveBranch; + } + + /** + * Sets which partial view script to use for rendering menu + * + * @param string|array $partial partial view script or null. If an array is + * given, it is expected to contain two + * values; the partial view script to use, + * and the module where the script can be + * found. + * @return self + */ + public function setPartial($partial) + { + if (null === $partial || is_string($partial) || is_array($partial)) { + $this->partial = $partial; + } + + return $this; + } + + /** + * Returns partial view script to use for rendering menu + * + * @return string|array|null + */ + public function getPartial() + { + return $this->partial; + } + + /** + * Enables/disables rendering of parents when only rendering active branch + * + * See {@link setOnlyActiveBranch()} for more information. + * + * @param bool $flag [optional] render parents when rendering active branch. + * @return self + */ + public function setRenderParents($flag = true) + { + $this->renderParents = (bool) $flag; + return $this; + } + + /** + * Returns flag indicating whether parents should be rendered when rendering + * only the active branch + * + * By default, this value is true. + * + * @return bool + */ + public function getRenderParents() + { + return $this->renderParents; + } + + /** + * Sets CSS class to use for the first 'ul' element when rendering + * + * @param string $ulClass CSS class to set + * @return self + */ + public function setUlClass($ulClass) + { + if (is_string($ulClass)) { + $this->ulClass = $ulClass; + } + + return $this; + } + + /** + * Returns CSS class to use for the first 'ul' element when rendering + * + * @return string + */ + public function getUlClass() + { + return $this->ulClass; + } + + /** + * Sets CSS class to use for the active 'li' element when rendering + * + * @param string $liActiveClass CSS class to set + * @return self + */ + public function setLiActiveClass($liActiveClass) + { + if (is_string($liActiveClass)) { + $this->liActiveClass = $liActiveClass; + } + + return $this; + } + + /** + * Returns CSS class to use for the active 'li' element when rendering + * + * @return string + */ + public function getLiActiveClass() + { + return $this->liActiveClass; + } +} diff --git a/library/Zend/View/Helper/Navigation/PluginManager.php b/library/Zend/View/Helper/Navigation/PluginManager.php new file mode 100755 index 0000000000..8faf86eb3d --- /dev/null +++ b/library/Zend/View/Helper/Navigation/PluginManager.php @@ -0,0 +1,58 @@ + 'Zend\View\Helper\Navigation\Breadcrumbs', + 'links' => 'Zend\View\Helper\Navigation\Links', + 'menu' => 'Zend\View\Helper\Navigation\Menu', + 'sitemap' => 'Zend\View\Helper\Navigation\Sitemap', + ); + + /** + * Validate the plugin + * + * Checks that the helper loaded is an instance of AbstractHelper. + * + * @param mixed $plugin + * @return void + * @throws Exception\InvalidArgumentException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof AbstractHelper) { + // we're okay + return; + } + + throw new Exception\InvalidArgumentException(sprintf( + 'Plugin of type %s is invalid; must implement %s\AbstractHelper', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/library/Zend/View/Helper/Navigation/Sitemap.php b/library/Zend/View/Helper/Navigation/Sitemap.php new file mode 100755 index 0000000000..2ba2b4a4bd --- /dev/null +++ b/library/Zend/View/Helper/Navigation/Sitemap.php @@ -0,0 +1,441 @@ + tag + * + * @var string + */ + const SITEMAP_NS = 'http://www.sitemaps.org/schemas/sitemap/0.9'; + + /** + * Schema URL + * + * @var string + */ + const SITEMAP_XSD = 'http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd'; + + /** + * Whether XML output should be formatted + * + * @var bool + */ + protected $formatOutput = false; + + /** + * Server url + * + * @var string + */ + protected $serverUrl; + + /** + * List of urls in the sitemap + * + * @var array + */ + protected $urls = array(); + + /** + * Whether sitemap should be validated using Zend\Validate\Sitemap\* + * + * @var bool + */ + protected $useSitemapValidators = true; + + /** + * Whether sitemap should be schema validated when generated + * + * @var bool + */ + protected $useSchemaValidation = false; + + /** + * Whether the XML declaration should be included in XML output + * + * @var bool + */ + protected $useXmlDeclaration = true; + + /** + * Helper entry point + * + * @param string|AbstractContainer $container container to operate on + * @return Sitemap + */ + public function __invoke($container = null) + { + if (null !== $container) { + $this->setContainer($container); + } + + return $this; + } + + /** + * Renders helper + * + * Implements {@link HelperInterface::render()}. + * + * @param AbstractContainer $container [optional] container to render. Default is + * to render the container registered in the helper. + * @return string + */ + public function render($container = null) + { + $dom = $this->getDomSitemap($container); + $xml = $this->getUseXmlDeclaration() ? + $dom->saveXML() : + $dom->saveXML($dom->documentElement); + + return rtrim($xml, PHP_EOL); + } + + /** + * Returns a DOMDocument containing the Sitemap XML for the given container + * + * @param AbstractContainer $container [optional] container to get + * breadcrumbs from, defaults + * to what is registered in the + * helper + * @return DOMDocument DOM representation of the + * container + * @throws Exception\RuntimeException if schema validation is on + * and the sitemap is invalid + * according to the sitemap + * schema, or if sitemap + * validators are used and the + * loc element fails validation + */ + public function getDomSitemap(AbstractContainer $container = null) + { + // Reset the urls + $this->urls = array(); + + if (null === $container) { + $container = $this->getContainer(); + } + + // check if we should validate using our own validators + if ($this->getUseSitemapValidators()) { + // create validators + $locValidator = new \Zend\Validator\Sitemap\Loc(); + $lastmodValidator = new \Zend\Validator\Sitemap\Lastmod(); + $changefreqValidator = new \Zend\Validator\Sitemap\Changefreq(); + $priorityValidator = new \Zend\Validator\Sitemap\Priority(); + } + + // create document + $dom = new DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = $this->getFormatOutput(); + + // ...and urlset (root) element + $urlSet = $dom->createElementNS(self::SITEMAP_NS, 'urlset'); + $dom->appendChild($urlSet); + + // create iterator + $iterator = new RecursiveIteratorIterator($container, + RecursiveIteratorIterator::SELF_FIRST); + + $maxDepth = $this->getMaxDepth(); + if (is_int($maxDepth)) { + $iterator->setMaxDepth($maxDepth); + } + $minDepth = $this->getMinDepth(); + if (!is_int($minDepth) || $minDepth < 0) { + $minDepth = 0; + } + + // iterate container + foreach ($iterator as $page) { + if ($iterator->getDepth() < $minDepth || !$this->accept($page)) { + // page should not be included + continue; + } + + // get absolute url from page + if (!$url = $this->url($page)) { + // skip page if it has no url (rare case) + // or already is in the sitemap + continue; + } + + // create url node for this page + $urlNode = $dom->createElementNS(self::SITEMAP_NS, 'url'); + $urlSet->appendChild($urlNode); + + if ($this->getUseSitemapValidators() + && !$locValidator->isValid($url) + ) { + throw new Exception\RuntimeException(sprintf( + 'Encountered an invalid URL for Sitemap XML: "%s"', + $url + )); + } + + // put url in 'loc' element + $urlNode->appendChild($dom->createElementNS(self::SITEMAP_NS, + 'loc', $url)); + + // add 'lastmod' element if a valid lastmod is set in page + if (isset($page->lastmod)) { + $lastmod = strtotime((string) $page->lastmod); + + // prevent 1970-01-01... + if ($lastmod !== false) { + $lastmod = date('c', $lastmod); + } + + if (!$this->getUseSitemapValidators() || + $lastmodValidator->isValid($lastmod)) { + $urlNode->appendChild( + $dom->createElementNS(self::SITEMAP_NS, 'lastmod', + $lastmod) + ); + } + } + + // add 'changefreq' element if a valid changefreq is set in page + if (isset($page->changefreq)) { + $changefreq = $page->changefreq; + if (!$this->getUseSitemapValidators() || + $changefreqValidator->isValid($changefreq)) { + $urlNode->appendChild( + $dom->createElementNS(self::SITEMAP_NS, 'changefreq', + $changefreq) + ); + } + } + + // add 'priority' element if a valid priority is set in page + if (isset($page->priority)) { + $priority = $page->priority; + if (!$this->getUseSitemapValidators() || + $priorityValidator->isValid($priority)) { + $urlNode->appendChild( + $dom->createElementNS(self::SITEMAP_NS, 'priority', $priority) + ); + } + } + } + + // validate using schema if specified + if ($this->getUseSchemaValidation()) { + ErrorHandler::start(); + $test = $dom->schemaValidate(self::SITEMAP_XSD); + $error = ErrorHandler::stop(); + if (!$test) { + throw new Exception\RuntimeException(sprintf( + 'Sitemap is invalid according to XML Schema at "%s"', + self::SITEMAP_XSD + ), 0, $error); + } + } + + return $dom; + } + + /** + * Returns an escaped absolute URL for the given page + * + * @param AbstractPage $page + * @return string + */ + public function url(AbstractPage $page) + { + $href = $page->getHref(); + + if (!isset($href{0})) { + // no href + return ''; + } elseif ($href{0} == '/') { + // href is relative to root; use serverUrl helper + $url = $this->getServerUrl() . $href; + } elseif (preg_match('/^[a-z]+:/im', (string) $href)) { + // scheme is given in href; assume absolute URL already + $url = (string) $href; + } else { + // href is relative to current document; use url helpers + $basePathHelper = $this->getView()->plugin('basepath'); + $curDoc = $basePathHelper(); + $curDoc = ('/' == $curDoc) ? '' : trim($curDoc, '/'); + $url = rtrim($this->getServerUrl(), '/') . '/' + . $curDoc + . (empty($curDoc) ? '' : '/') . $href; + } + + if (! in_array($url, $this->urls)) { + $this->urls[] = $url; + return $this->xmlEscape($url); + } + + return null; + } + + /** + * Escapes string for XML usage + * + * @param string $string + * @return string + */ + protected function xmlEscape($string) + { + $escaper = $this->view->plugin('escapeHtml'); + return $escaper($string); + } + + /** + * Sets whether XML output should be formatted + * + * @param bool $formatOutput + * @return Sitemap + */ + public function setFormatOutput($formatOutput = true) + { + $this->formatOutput = (bool) $formatOutput; + return $this; + } + + /** + * Returns whether XML output should be formatted + * + * @return bool + */ + public function getFormatOutput() + { + return $this->formatOutput; + } + + /** + * Sets server url (scheme and host-related stuff without request URI) + * + * E.g. http://www.example.com + * + * @param string $serverUrl + * @return Sitemap + * @throws Exception\InvalidArgumentException + */ + public function setServerUrl($serverUrl) + { + $uri = Uri\UriFactory::factory($serverUrl); + $uri->setFragment(''); + $uri->setPath(''); + $uri->setQuery(''); + + if ($uri->isValid()) { + $this->serverUrl = $uri->toString(); + } else { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid server URL: "%s"', + $serverUrl + )); + } + + return $this; + } + + /** + * Returns server URL + * + * @return string + */ + public function getServerUrl() + { + if (!isset($this->serverUrl)) { + $serverUrlHelper = $this->getView()->plugin('serverUrl'); + $this->serverUrl = $serverUrlHelper(); + } + + return $this->serverUrl; + } + + /** + * Sets whether sitemap should be validated using Zend\Validate\Sitemap_* + * + * @param bool $useSitemapValidators + * @return Sitemap + */ + public function setUseSitemapValidators($useSitemapValidators) + { + $this->useSitemapValidators = (bool) $useSitemapValidators; + return $this; + } + + /** + * Returns whether sitemap should be validated using Zend\Validate\Sitemap_* + * + * @return bool + */ + public function getUseSitemapValidators() + { + return $this->useSitemapValidators; + } + + /** + * Sets whether sitemap should be schema validated when generated + * + * @param bool $schemaValidation + * @return Sitemap + */ + public function setUseSchemaValidation($schemaValidation) + { + $this->useSchemaValidation = (bool) $schemaValidation; + return $this; + } + + /** + * Returns true if sitemap should be schema validated when generated + * + * @return bool + */ + public function getUseSchemaValidation() + { + return $this->useSchemaValidation; + } + + /** + * Sets whether the XML declaration should be used in output + * + * @param bool $useXmlDecl + * @return Sitemap + */ + public function setUseXmlDeclaration($useXmlDecl) + { + $this->useXmlDeclaration = (bool) $useXmlDecl; + return $this; + } + + /** + * Returns whether the XML declaration should be used in output + * + * @return bool + */ + public function getUseXmlDeclaration() + { + return $this->useXmlDeclaration; + } +} diff --git a/library/Zend/View/Helper/PaginationControl.php b/library/Zend/View/Helper/PaginationControl.php new file mode 100755 index 0000000000..9963fdd35f --- /dev/null +++ b/library/Zend/View/Helper/PaginationControl.php @@ -0,0 +1,131 @@ +paginator is set and, + * if so, uses that. Also, if no scrolling style or partial are specified, + * the defaults will be used (if set). + * + * @param Paginator\Paginator $paginator (Optional) + * @param string $scrollingStyle (Optional) Scrolling style + * @param string $partial (Optional) View partial + * @param array|string $params (Optional) params to pass to the partial + * @throws Exception\RuntimeException if no paginator or no view partial provided + * @throws Exception\InvalidArgumentException if partial is invalid array + * @return string + */ + public function __invoke(Paginator\Paginator $paginator = null, $scrollingStyle = null, $partial = null, $params = null) + { + if ($paginator === null) { + if (isset($this->view->paginator) and $this->view->paginator !== null and $this->view->paginator instanceof Paginator\Paginator) { + $paginator = $this->view->paginator; + } else { + throw new Exception\RuntimeException('No paginator instance provided or incorrect type'); + } + } + + if ($partial === null) { + if (static::$defaultViewPartial === null) { + throw new Exception\RuntimeException('No view partial provided and no default set'); + } + + $partial = static::$defaultViewPartial; + } + + if ($scrollingStyle === null) { + $scrollingStyle = static::$defaultScrollingStyle; + } + + $pages = get_object_vars($paginator->getPages($scrollingStyle)); + + if ($params !== null) { + $pages = array_merge($pages, (array) $params); + } + + if (is_array($partial)) { + if (count($partial) != 2) { + throw new Exception\InvalidArgumentException( + 'A view partial supplied as an array must contain two values: the filename and its module' + ); + } + + if ($partial[1] !== null) { + $partialHelper = $this->view->plugin('partial'); + return $partialHelper($partial[0], $pages); + } + + $partial = $partial[0]; + } + + $partialHelper = $this->view->plugin('partial'); + return $partialHelper($partial, $pages); + } + + /** + * Sets the default Scrolling Style + * + * @param string $style string 'all' | 'elastic' | 'sliding' | 'jumping' + */ + public static function setDefaultScrollingStyle($style) + { + static::$defaultScrollingStyle = $style; + } + + /** + * Gets the default scrolling style + * + * @return string + */ + public static function getDefaultScrollingStyle() + { + return static::$defaultScrollingStyle; + } + + /** + * Sets the default view partial. + * + * @param string|array $partial View partial + */ + public static function setDefaultViewPartial($partial) + { + static::$defaultViewPartial = $partial; + } + + /** + * Gets the default view partial + * + * @return string|array + */ + public static function getDefaultViewPartial() + { + return static::$defaultViewPartial; + } +} diff --git a/library/Zend/View/Helper/Partial.php b/library/Zend/View/Helper/Partial.php new file mode 100755 index 0000000000..444f161b98 --- /dev/null +++ b/library/Zend/View/Helper/Partial.php @@ -0,0 +1,94 @@ +getView()->render($name); + } + + if (is_scalar($values)) { + $values = array(); + } elseif ($values instanceof ModelInterface) { + $values = $values->getVariables(); + } elseif (is_object($values)) { + if (null !== ($objectKey = $this->getObjectKey())) { + $values = array($objectKey => $values); + } elseif (method_exists($values, 'toArray')) { + $values = $values->toArray(); + } else { + $values = get_object_vars($values); + } + } + + return $this->getView()->render($name, $values); + } + + /** + * Set object key + * + * @param string $key + * @return Partial + */ + public function setObjectKey($key) + { + if (null === $key) { + $this->objectKey = null; + return $this; + } + + $this->objectKey = (string) $key; + + return $this; + } + + /** + * Retrieve object key + * + * The objectKey is the variable to which an object in the iterator will be + * assigned. + * + * @return null|string + */ + public function getObjectKey() + { + return $this->objectKey; + } +} diff --git a/library/Zend/View/Helper/PartialLoop.php b/library/Zend/View/Helper/PartialLoop.php new file mode 100755 index 0000000000..f66297e67b --- /dev/null +++ b/library/Zend/View/Helper/PartialLoop.php @@ -0,0 +1,77 @@ +toArray(); + } else { + throw new Exception\InvalidArgumentException('PartialLoop helper requires iterable data'); + } + } + + // reset the counter if it's called again + $this->partialCounter = 0; + $content = ''; + + foreach ($values as $item) { + $this->partialCounter++; + $content .= parent::__invoke($name, $item); + } + + return $content; + } + + /** + * Get the partial counter + * + * @return int + */ + public function getPartialCounter() + { + return $this->partialCounter; + } +} diff --git a/library/Zend/View/Helper/Placeholder.php b/library/Zend/View/Helper/Placeholder.php new file mode 100755 index 0000000000..297c17a554 --- /dev/null +++ b/library/Zend/View/Helper/Placeholder.php @@ -0,0 +1,98 @@ +getContainer($name); + } + + /** + * createContainer + * + * @param string $key + * @param array $value + * @return Container\AbstractContainer + */ + public function createContainer($key, array $value = array()) + { + $key = (string) $key; + + $this->items[$key] = new $this->containerClass($value); + return $this->items[$key]; + } + + /** + * Retrieve a placeholder container + * + * @param string $key + * @return Container\AbstractContainer + */ + public function getContainer($key) + { + $key = (string) $key; + if (isset($this->items[$key])) { + return $this->items[$key]; + } + + $container = $this->createContainer($key); + + return $container; + } + + /** + * Does a particular container exist? + * + * @param string $key + * @return bool + */ + public function containerExists($key) + { + $key = (string) $key; + $return = array_key_exists($key, $this->items); + return $return; + } +} diff --git a/library/Zend/View/Helper/Placeholder/Container.php b/library/Zend/View/Helper/Placeholder/Container.php new file mode 100755 index 0000000000..35220176c8 --- /dev/null +++ b/library/Zend/View/Helper/Placeholder/Container.php @@ -0,0 +1,17 @@ +toString(); + } + + /** + * Render the placeholder + * + * @param null|int|string $indent + * @return string + */ + public function toString($indent = null) + { + $indent = ($indent !== null) + ? $this->getWhitespace($indent) + : $this->getIndent(); + + $items = $this->getArrayCopy(); + $return = $indent + . $this->getPrefix() + . implode($this->getSeparator(), $items) + . $this->getPostfix(); + $return = preg_replace("/(\r\n?|\n)/", '$1' . $indent, $return); + + return $return; + } + + /** + * Start capturing content to push into placeholder + * + * @param string $type How to capture content into placeholder; append, prepend, or set + * @param mixed $key Key to which to capture content + * @throws Exception\RuntimeException if nested captures detected + * @return void + */ + public function captureStart($type = AbstractContainer::APPEND, $key = null) + { + if ($this->captureLock) { + throw new Exception\RuntimeException( + 'Cannot nest placeholder captures for the same placeholder' + ); + } + + $this->captureLock = true; + $this->captureType = $type; + if ((null !== $key) && is_scalar($key)) { + $this->captureKey = (string) $key; + } + ob_start(); + } + + /** + * End content capture + * + * @return void + */ + public function captureEnd() + { + $data = ob_get_clean(); + $key = null; + $this->captureLock = false; + if (null !== $this->captureKey) { + $key = $this->captureKey; + } + switch ($this->captureType) { + case self::SET: + if (null !== $key) { + $this[$key] = $data; + } else { + $this->exchangeArray(array($data)); + } + break; + case self::PREPEND: + if (null !== $key) { + $array = array($key => $data); + $values = $this->getArrayCopy(); + $final = $array + $values; + $this->exchangeArray($final); + } else { + $this->prepend($data); + } + break; + case self::APPEND: + default: + if (null !== $key) { + if (empty($this[$key])) { + $this[$key] = $data; + } else { + $this[$key] .= $data; + } + } else { + $this[$this->nextIndex()] = $data; + } + break; + } + } + + /** + * Get keys + * + * @return array + */ + public function getKeys() + { + $array = $this->getArrayCopy(); + + return array_keys($array); + } + + /** + * Retrieve container value + * + * If single element registered, returns that element; otherwise, + * serializes to array. + * + * @return mixed + */ + public function getValue() + { + if (1 == count($this)) { + $keys = $this->getKeys(); + $key = array_shift($keys); + return $this[$key]; + } + + return $this->getArrayCopy(); + } + + /** + * Retrieve whitespace representation of $indent + * + * @param int|string $indent + * @return string + */ + public function getWhitespace($indent) + { + if (is_int($indent)) { + $indent = str_repeat(' ', $indent); + } + + return (string) $indent; + } + + /** + * Set a single value + * + * @param mixed $value + * @return void + */ + public function set($value) + { + $this->exchangeArray(array($value)); + + return $this; + } + + /** + * Prepend a value to the top of the container + * + * @param mixed $value + * @return self + */ + public function prepend($value) + { + $values = $this->getArrayCopy(); + array_unshift($values, $value); + $this->exchangeArray($values); + + return $this; + } + + /** + * Append a value to the end of the container + * + * @param mixed $value + * @return self + */ + public function append($value) + { + parent::append($value); + return $this; + } + + /** + * Next Index as defined by the PHP manual + * + * @return int + */ + public function nextIndex() + { + $keys = $this->getKeys(); + if (0 == count($keys)) { + return 0; + } + + return $nextIndex = max($keys) + 1; + } + + /** + * Set the indentation string for __toString() serialization, + * optionally, if a number is passed, it will be the number of spaces + * + * @param string|int $indent + * @return self + */ + public function setIndent($indent) + { + $this->indent = $this->getWhitespace($indent); + return $this; + } + + /** + * Retrieve indentation + * + * @return string + */ + public function getIndent() + { + return $this->indent; + } + + /** + * Set postfix for __toString() serialization + * + * @param string $postfix + * @return self + */ + public function setPostfix($postfix) + { + $this->postfix = (string) $postfix; + return $this; + } + + /** + * Retrieve postfix + * + * @return string + */ + public function getPostfix() + { + return $this->postfix; + } + + /** + * Set prefix for __toString() serialization + * + * @param string $prefix + * @return self + */ + public function setPrefix($prefix) + { + $this->prefix = (string) $prefix; + return $this; + } + + /** + * Retrieve prefix + * + * @return string + */ + public function getPrefix() + { + return $this->prefix; + } + + /** + * Set separator for __toString() serialization + * + * Used to implode elements in container + * + * @param string $separator + * @return self + */ + public function setSeparator($separator) + { + $this->separator = (string) $separator; + return $this; + } + + /** + * Retrieve separator + * + * @return string + */ + public function getSeparator() + { + return $this->separator; + } +} diff --git a/library/Zend/View/Helper/Placeholder/Container/AbstractStandalone.php b/library/Zend/View/Helper/Placeholder/Container/AbstractStandalone.php new file mode 100755 index 0000000000..619ba70737 --- /dev/null +++ b/library/Zend/View/Helper/Placeholder/Container/AbstractStandalone.php @@ -0,0 +1,376 @@ +setContainer($this->getContainer()); + } + + /** + * Overload + * + * Proxy to container methods + * + * @param string $method + * @param array $args + * @throws Exception\BadMethodCallException + * @return mixed + */ + public function __call($method, $args) + { + $container = $this->getContainer(); + if (method_exists($container, $method)) { + $return = call_user_func_array(array($container, $method), $args); + if ($return === $container) { + // If the container is returned, we really want the current object + return $this; + } + return $return; + } + + throw new Exception\BadMethodCallException('Method "' . $method . '" does not exist'); + } + + /** + * Overloading: set property value + * + * @param string $key + * @param mixed $value + * @return void + */ + public function __set($key, $value) + { + $container = $this->getContainer(); + $container[$key] = $value; + } + + /** + * Overloading: retrieve property + * + * @param string $key + * @return mixed + */ + public function __get($key) + { + $container = $this->getContainer(); + if (isset($container[$key])) { + return $container[$key]; + } + + return null; + } + + /** + * Overloading: check if property is set + * + * @param string $key + * @return bool + */ + public function __isset($key) + { + $container = $this->getContainer(); + return isset($container[$key]); + } + + /** + * Overloading: unset property + * + * @param string $key + * @return void + */ + public function __unset($key) + { + $container = $this->getContainer(); + if (isset($container[$key])) { + unset($container[$key]); + } + } + + /** + * Cast to string representation + * + * @return string + */ + public function __toString() + { + return $this->toString(); + } + + /** + * String representation + * + * @return string + */ + public function toString() + { + return $this->getContainer()->toString(); + } + + /** + * Escape a string + * + * @param string $string + * @return string + */ + protected function escape($string) + { + if ($this->getView() instanceof RendererInterface + && method_exists($this->getView(), 'getEncoding') + ) { + $escaper = $this->getView()->plugin('escapeHtml'); + return $escaper((string) $string); + } + + return $this->getEscaper()->escapeHtml((string) $string); + } + + /** + * Set whether or not auto escaping should be used + * + * @param bool $autoEscape whether or not to auto escape output + * @return AbstractStandalone + */ + public function setAutoEscape($autoEscape = true) + { + $this->autoEscape = ($autoEscape) ? true : false; + return $this; + } + + /** + * Return whether autoEscaping is enabled or disabled + * + * return bool + */ + public function getAutoEscape() + { + return $this->autoEscape; + } + + /** + * Set container on which to operate + * + * @param AbstractContainer $container + * @return AbstractStandalone + */ + public function setContainer(AbstractContainer $container) + { + $this->container = $container; + return $this; + } + + /** + * Retrieve placeholder container + * + * @return AbstractContainer + */ + public function getContainer() + { + if (!$this->container instanceof AbstractContainer) { + $this->container = new $this->containerClass(); + } + return $this->container; + } + + /** + * Delete a container + * + * @return bool + */ + public function deleteContainer() + { + if (null != $this->container) { + $this->container = null; + return true; + } + + return false; + } + + /** + * Set the container class to use + * + * @param string $name + * @throws Exception\InvalidArgumentException + * @throws Exception\DomainException + * @return \Zend\View\Helper\Placeholder\Container\AbstractStandalone + */ + public function setContainerClass($name) + { + if (!class_exists($name)) { + throw new Exception\DomainException( + sprintf( + '%s expects a valid container class name; received "%s", which did not resolve', + __METHOD__, + $name + ) + ); + } + + if (!in_array('Zend\View\Helper\Placeholder\Container\AbstractContainer', class_parents($name))) { + throw new Exception\InvalidArgumentException('Invalid Container class specified'); + } + + $this->containerClass = $name; + return $this; + } + + /** + * Retrieve the container class + * + * @return string + */ + public function getContainerClass() + { + return $this->containerClass; + } + + /** + * Set Escaper instance + * + * @param Escaper $escaper + * @return AbstractStandalone + */ + public function setEscaper(Escaper $escaper) + { + $encoding = $escaper->getEncoding(); + $this->escapers[$encoding] = $escaper; + + return $this; + } + + /** + * Get Escaper instance + * + * Lazy-loads one if none available + * + * @param string|null $enc Encoding to use + * @return mixed + */ + public function getEscaper($enc = 'UTF-8') + { + $enc = strtolower($enc); + if (!isset($this->escapers[$enc])) { + $this->setEscaper(new Escaper($enc)); + } + + return $this->escapers[$enc]; + } + + /** + * Countable + * + * @return int + */ + public function count() + { + $container = $this->getContainer(); + return count($container); + } + + /** + * ArrayAccess: offsetExists + * + * @param string|int $offset + * @return bool + */ + public function offsetExists($offset) + { + return $this->getContainer()->offsetExists($offset); + } + + /** + * ArrayAccess: offsetGet + * + * @param string|int $offset + * @return mixed + */ + public function offsetGet($offset) + { + return $this->getContainer()->offsetGet($offset); + } + + /** + * ArrayAccess: offsetSet + * + * @param string|int $offset + * @param mixed $value + * @return void + */ + public function offsetSet($offset, $value) + { + return $this->getContainer()->offsetSet($offset, $value); + } + + /** + * ArrayAccess: offsetUnset + * + * @param string|int $offset + * @return void + */ + public function offsetUnset($offset) + { + return $this->getContainer()->offsetUnset($offset); + } + + /** + * IteratorAggregate: get Iterator + * + * @return \Iterator + */ + public function getIterator() + { + return $this->getContainer()->getIterator(); + } +} diff --git a/library/Zend/View/Helper/Placeholder/Registry.php b/library/Zend/View/Helper/Placeholder/Registry.php new file mode 100755 index 0000000000..f86b4e1ad6 --- /dev/null +++ b/library/Zend/View/Helper/Placeholder/Registry.php @@ -0,0 +1,185 @@ +items[$key] = $container; + + return $this; + } + + /** + * Retrieve a placeholder container + * + * @param string $key + * @return Container\AbstractContainer + */ + public function getContainer($key) + { + $key = (string) $key; + if (isset($this->items[$key])) { + return $this->items[$key]; + } + + $container = $this->createContainer($key); + + return $container; + } + + /** + * Does a particular container exist? + * + * @param string $key + * @return bool + */ + public function containerExists($key) + { + $key = (string) $key; + + return array_key_exists($key, $this->items); + } + + /** + * createContainer + * + * @param string $key + * @param array $value + * @return Container\AbstractContainer + */ + public function createContainer($key, array $value = array()) + { + $key = (string) $key; + + $this->items[$key] = new $this->containerClass($value); + + return $this->items[$key]; + } + + /** + * Delete a container + * + * @param string $key + * @return bool + */ + public function deleteContainer($key) + { + $key = (string) $key; + if (isset($this->items[$key])) { + unset($this->items[$key]); + return true; + } + + return false; + } + + /** + * Set the container class to use + * + * @param string $name + * @throws Exception\InvalidArgumentException + * @throws Exception\DomainException + * @return Registry + */ + public function setContainerClass($name) + { + if (!class_exists($name)) { + throw new Exception\DomainException( + sprintf( + '%s expects a valid registry class name; received "%s", which did not resolve', + __METHOD__, + $name + ) + ); + } + + if (!in_array('Zend\View\Helper\Placeholder\Container\AbstractContainer', class_parents($name))) { + throw new Exception\InvalidArgumentException('Invalid Container class specified'); + } + + $this->containerClass = $name; + + return $this; + } + + /** + * Retrieve the container class + * + * @return string + */ + public function getContainerClass() + { + return $this->containerClass; + } +} diff --git a/library/Zend/View/Helper/RenderChildModel.php b/library/Zend/View/Helper/RenderChildModel.php new file mode 100755 index 0000000000..d59edfe055 --- /dev/null +++ b/library/Zend/View/Helper/RenderChildModel.php @@ -0,0 +1,133 @@ +render($child); + } + + /** + * Render a model + * + * If a matching child model is found, it is rendered. If not, an empty + * string is returned. + * + * @param string $child + * @return string + */ + public function render($child) + { + $model = $this->findChild($child); + if (!$model) { + return ''; + } + + $current = $this->current; + $view = $this->getView(); + $return = $view->render($model); + $helper = $this->getViewModelHelper(); + $helper->setCurrent($current); + + return $return; + } + + /** + * Find the named child model + * + * Iterates through the current view model, looking for a child model that + * has a captureTo value matching the requested $child. If found, that child + * model is returned; otherwise, a boolean false is returned. + * + * @param string $child + * @return false|Model + */ + protected function findChild($child) + { + $this->current = $model = $this->getCurrent(); + foreach ($model->getChildren() as $childModel) { + if ($childModel->captureTo() == $child) { + return $childModel; + } + } + + return false; + } + + /** + * Get the current view model + * + * @throws Exception\RuntimeException + * @return null|Model + */ + protected function getCurrent() + { + $helper = $this->getViewModelHelper(); + if (!$helper->hasCurrent()) { + throw new Exception\RuntimeException(sprintf( + '%s: no view model currently registered in renderer; cannot query for children', + __METHOD__ + )); + } + + return $helper->getCurrent(); + } + + /** + * Retrieve the view model helper + * + * @return ViewModel + */ + protected function getViewModelHelper() + { + if ($this->viewModelHelper) { + return $this->viewModelHelper; + } + + if (method_exists($this->getView(), 'plugin')) { + $this->viewModelHelper = $this->view->plugin('view_model'); + } + + return $this->viewModelHelper; + } +} diff --git a/library/Zend/View/Helper/RenderToPlaceholder.php b/library/Zend/View/Helper/RenderToPlaceholder.php new file mode 100755 index 0000000000..3707995b33 --- /dev/null +++ b/library/Zend/View/Helper/RenderToPlaceholder.php @@ -0,0 +1,35 @@ +view->plugin('placeholder'); + $placeholderHelper($placeholder)->captureStart(); + echo $this->view->render($script); + $placeholderHelper($placeholder)->captureEnd(); + } +} diff --git a/library/Zend/View/Helper/ServerUrl.php b/library/Zend/View/Helper/ServerUrl.php new file mode 100755 index 0000000000..ae846a9baf --- /dev/null +++ b/library/Zend/View/Helper/ServerUrl.php @@ -0,0 +1,330 @@ +getScheme() . '://' . $this->getHost() . $path; + } + + /** + * Detect the host based on headers + * + * @return void + */ + protected function detectHost() + { + if ($this->setHostFromProxy()) { + return; + } + + if (isset($_SERVER['HTTP_HOST']) && !empty($_SERVER['HTTP_HOST'])) { + // Detect if the port is set in SERVER_PORT and included in HTTP_HOST + if (isset($_SERVER['SERVER_PORT'])) { + $portStr = ':' . $_SERVER['SERVER_PORT']; + if (substr($_SERVER['HTTP_HOST'], 0-strlen($portStr), strlen($portStr)) == $portStr) { + $this->setHost(substr($_SERVER['HTTP_HOST'], 0, 0-strlen($portStr))); + return; + } + } + + $this->setHost($_SERVER['HTTP_HOST']); + + return; + } + + if (!isset($_SERVER['SERVER_NAME']) || !isset($_SERVER['SERVER_PORT'])) { + return; + } + + $name = $_SERVER['SERVER_NAME']; + $this->setHost($name); + } + + /** + * Detect the port + * + * @return null + */ + protected function detectPort() + { + if ($this->setPortFromProxy()) { + return; + } + + if (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT']) { + $this->setPort($_SERVER['SERVER_PORT']); + return; + } + } + + /** + * Detect the scheme + * + * @return null + */ + protected function detectScheme() + { + if ($this->setSchemeFromProxy()) { + return; + } + + switch (true) { + case (isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] === true)): + case (isset($_SERVER['HTTP_SCHEME']) && ($_SERVER['HTTP_SCHEME'] == 'https')): + case (443 === $this->getPort()): + $scheme = 'https'; + break; + default: + $scheme = 'http'; + break; + } + + $this->setScheme($scheme); + } + + /** + * Detect if a proxy is in use, and, if so, set the host based on it + * + * @return bool + */ + protected function setHostFromProxy() + { + if (!$this->useProxy) { + return false; + } + + if (!isset($_SERVER['HTTP_X_FORWARDED_HOST']) || empty($_SERVER['HTTP_X_FORWARDED_HOST'])) { + return false; + } + + $host = $_SERVER['HTTP_X_FORWARDED_HOST']; + if (strpos($host, ',') !== false) { + $hosts = explode(',', $host); + $host = trim(array_pop($hosts)); + } + if (empty($host)) { + return false; + } + $this->setHost($host); + + return true; + } + + /** + * Set port based on detected proxy headers + * + * @return bool + */ + protected function setPortFromProxy() + { + if (!$this->useProxy) { + return false; + } + + if (!isset($_SERVER['HTTP_X_FORWARDED_PORT']) || empty($_SERVER['HTTP_X_FORWARDED_PORT'])) { + return false; + } + + $port = $_SERVER['HTTP_X_FORWARDED_PORT']; + $this->setPort($port); + + return true; + } + + /** + * Set the current scheme based on detected proxy headers + * + * @return bool + */ + protected function setSchemeFromProxy() + { + if (!$this->useProxy) { + return false; + } + + if (isset($_SERVER['SSL_HTTPS'])) { + $sslHttps = strtolower($_SERVER['SSL_HTTPS']); + if (in_array($sslHttps, array('on', 1))) { + $this->setScheme('https'); + return true; + } + } + + if (!isset($_SERVER['HTTP_X_FORWARDED_PROTO']) || empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) { + return false; + } + + $scheme = trim(strtolower($_SERVER['HTTP_X_FORWARDED_PROTO'])); + if (empty($scheme)) { + return false; + } + + $this->setScheme($scheme); + + return true; + } + + /** + * Sets host + * + * @param string $host + * @return ServerUrl + */ + public function setHost($host) + { + $port = $this->getPort(); + $scheme = $this->getScheme(); + + if (($scheme == 'http' && (null === $port || $port == 80)) + || ($scheme == 'https' && (null === $port || $port == 443)) + ) { + $this->host = $host; + return $this; + } + + $this->host = $host . ':' . $port; + + return $this; + } + + /** + * Returns host + * + * @return string + */ + public function getHost() + { + if (null === $this->host) { + $this->detectHost(); + } + + return $this->host; + } + + /** + * Set server port + * + * @param int $port + * @return ServerUrl + */ + public function setPort($port) + { + $this->port = (int) $port; + + return $this; + } + + /** + * Retrieve the server port + * + * @return int|null + */ + public function getPort() + { + if (null === $this->port) { + $this->detectPort(); + } + + return $this->port; + } + + /** + * Sets scheme (typically http or https) + * + * @param string $scheme + * @return ServerUrl + */ + public function setScheme($scheme) + { + $this->scheme = $scheme; + + return $this; + } + + /** + * Returns scheme (typically http or https) + * + * @return string + */ + public function getScheme() + { + if (null === $this->scheme) { + $this->detectScheme(); + } + + return $this->scheme; + } + + /** + * Set flag indicating whether or not to query proxy servers + * + * @param bool $useProxy + * @return ServerUrl + */ + public function setUseProxy($useProxy = false) + { + $this->useProxy = (bool) $useProxy; + + return $this; + } +} diff --git a/library/Zend/View/Helper/Service/FlashMessengerFactory.php b/library/Zend/View/Helper/Service/FlashMessengerFactory.php new file mode 100755 index 0000000000..fbaf53e8bc --- /dev/null +++ b/library/Zend/View/Helper/Service/FlashMessengerFactory.php @@ -0,0 +1,47 @@ +getServiceLocator(); + $helper = new FlashMessenger(); + $controllerPluginManager = $serviceLocator->get('ControllerPluginManager'); + $flashMessenger = $controllerPluginManager->get('flashmessenger'); + $helper->setPluginFlashMessenger($flashMessenger); + $config = $serviceLocator->get('Config'); + if (isset($config['view_helper_config']['flashmessenger'])) { + $configHelper = $config['view_helper_config']['flashmessenger']; + if (isset($configHelper['message_open_format'])) { + $helper->setMessageOpenFormat($configHelper['message_open_format']); + } + if (isset($configHelper['message_separator_string'])) { + $helper->setMessageSeparatorString($configHelper['message_separator_string']); + } + if (isset($configHelper['message_close_string'])) { + $helper->setMessageCloseString($configHelper['message_close_string']); + } + } + + return $helper; + } +} diff --git a/library/Zend/View/Helper/Service/IdentityFactory.php b/library/Zend/View/Helper/Service/IdentityFactory.php new file mode 100755 index 0000000000..a065a1d9df --- /dev/null +++ b/library/Zend/View/Helper/Service/IdentityFactory.php @@ -0,0 +1,32 @@ +getServiceLocator(); + $helper = new Identity(); + if ($services->has('Zend\Authentication\AuthenticationService')) { + $helper->setAuthenticationService($services->get('Zend\Authentication\AuthenticationService')); + } + return $helper; + } +} diff --git a/library/Zend/View/Helper/Url.php b/library/Zend/View/Helper/Url.php new file mode 100755 index 0000000000..9b2af7e97c --- /dev/null +++ b/library/Zend/View/Helper/Url.php @@ -0,0 +1,126 @@ +router) { + throw new Exception\RuntimeException('No RouteStackInterface instance provided'); + } + + if (3 == func_num_args() && is_bool($options)) { + $reuseMatchedParams = $options; + $options = array(); + } + + if ($name === null) { + if ($this->routeMatch === null) { + throw new Exception\RuntimeException('No RouteMatch instance provided'); + } + + $name = $this->routeMatch->getMatchedRouteName(); + + if ($name === null) { + throw new Exception\RuntimeException('RouteMatch does not contain a matched route name'); + } + } + + if (!is_array($params)) { + if (!$params instanceof Traversable) { + throw new Exception\InvalidArgumentException( + 'Params is expected to be an array or a Traversable object' + ); + } + $params = iterator_to_array($params); + } + + if ($reuseMatchedParams && $this->routeMatch !== null) { + $routeMatchParams = $this->routeMatch->getParams(); + + if (isset($routeMatchParams[ModuleRouteListener::ORIGINAL_CONTROLLER])) { + $routeMatchParams['controller'] = $routeMatchParams[ModuleRouteListener::ORIGINAL_CONTROLLER]; + unset($routeMatchParams[ModuleRouteListener::ORIGINAL_CONTROLLER]); + } + + if (isset($routeMatchParams[ModuleRouteListener::MODULE_NAMESPACE])) { + unset($routeMatchParams[ModuleRouteListener::MODULE_NAMESPACE]); + } + + $params = array_merge($routeMatchParams, $params); + } + + $options['name'] = $name; + + return $this->router->assemble($params, $options); + } + + /** + * Set the router to use for assembling. + * + * @param RouteStackInterface $router + * @return Url + */ + public function setRouter(RouteStackInterface $router) + { + $this->router = $router; + return $this; + } + + /** + * Set route match returned by the router. + * + * @param RouteMatch $routeMatch + * @return Url + */ + public function setRouteMatch(RouteMatch $routeMatch) + { + $this->routeMatch = $routeMatch; + return $this; + } +} diff --git a/library/Zend/View/Helper/ViewModel.php b/library/Zend/View/Helper/ViewModel.php new file mode 100755 index 0000000000..49b1306496 --- /dev/null +++ b/library/Zend/View/Helper/ViewModel.php @@ -0,0 +1,92 @@ +current = $model; + return $this; + } + + /** + * Get the current view model + * + * @return null|Model + */ + public function getCurrent() + { + return $this->current; + } + + /** + * Is a current view model composed? + * + * @return bool + */ + public function hasCurrent() + { + return ($this->current instanceof Model); + } + + /** + * Set the root view model + * + * @param Model $model + * @return ViewModel + */ + public function setRoot(Model $model) + { + $this->root = $model; + return $this; + } + + /** + * Get the root view model + * + * @return null|Model + */ + public function getRoot() + { + return $this->root; + } + + /** + * Is a root view model composed? + * + * @return bool + */ + public function hasRoot() + { + return ($this->root instanceof Model); + } +} diff --git a/library/Zend/View/HelperPluginManager.php b/library/Zend/View/HelperPluginManager.php new file mode 100755 index 0000000000..d2ddc69047 --- /dev/null +++ b/library/Zend/View/HelperPluginManager.php @@ -0,0 +1,194 @@ + 'Zend\View\Helper\Service\FlashMessengerFactory', + 'identity' => 'Zend\View\Helper\Service\IdentityFactory', + ); + + /** + * Default set of helpers + * + * @var array + */ + protected $invokableClasses = array( + // basepath, doctype, and url are set up as factories in the ViewHelperManagerFactory. + // basepath and url are not very useful without their factories, however the doctype + // helper works fine as an invokable. The factory for doctype simply checks for the + // config value from the merged config. + 'basepath' => 'Zend\View\Helper\BasePath', + 'cycle' => 'Zend\View\Helper\Cycle', + 'declarevars' => 'Zend\View\Helper\DeclareVars', + 'doctype' => 'Zend\View\Helper\Doctype', // overridden by a factory in ViewHelperManagerFactory + 'escapehtml' => 'Zend\View\Helper\EscapeHtml', + 'escapehtmlattr' => 'Zend\View\Helper\EscapeHtmlAttr', + 'escapejs' => 'Zend\View\Helper\EscapeJs', + 'escapecss' => 'Zend\View\Helper\EscapeCss', + 'escapeurl' => 'Zend\View\Helper\EscapeUrl', + 'gravatar' => 'Zend\View\Helper\Gravatar', + 'headlink' => 'Zend\View\Helper\HeadLink', + 'headmeta' => 'Zend\View\Helper\HeadMeta', + 'headscript' => 'Zend\View\Helper\HeadScript', + 'headstyle' => 'Zend\View\Helper\HeadStyle', + 'headtitle' => 'Zend\View\Helper\HeadTitle', + 'htmlflash' => 'Zend\View\Helper\HtmlFlash', + 'htmllist' => 'Zend\View\Helper\HtmlList', + 'htmlobject' => 'Zend\View\Helper\HtmlObject', + 'htmlpage' => 'Zend\View\Helper\HtmlPage', + 'htmlquicktime' => 'Zend\View\Helper\HtmlQuicktime', + 'inlinescript' => 'Zend\View\Helper\InlineScript', + 'json' => 'Zend\View\Helper\Json', + 'layout' => 'Zend\View\Helper\Layout', + 'paginationcontrol' => 'Zend\View\Helper\PaginationControl', + 'partialloop' => 'Zend\View\Helper\PartialLoop', + 'partial' => 'Zend\View\Helper\Partial', + 'placeholder' => 'Zend\View\Helper\Placeholder', + 'renderchildmodel' => 'Zend\View\Helper\RenderChildModel', + 'rendertoplaceholder' => 'Zend\View\Helper\RenderToPlaceholder', + 'serverurl' => 'Zend\View\Helper\ServerUrl', + 'url' => 'Zend\View\Helper\Url', + 'viewmodel' => 'Zend\View\Helper\ViewModel', + ); + + /** + * @var Renderer\RendererInterface + */ + protected $renderer; + + /** + * Constructor + * + * After invoking parent constructor, add an initializer to inject the + * attached renderer and translator, if any, to the currently requested helper. + * + * @param null|ConfigInterface $configuration + */ + public function __construct(ConfigInterface $configuration = null) + { + parent::__construct($configuration); + + $this->addInitializer(array($this, 'injectRenderer')) + ->addInitializer(array($this, 'injectTranslator')); + } + + /** + * Set renderer + * + * @param Renderer\RendererInterface $renderer + * @return HelperPluginManager + */ + public function setRenderer(Renderer\RendererInterface $renderer) + { + $this->renderer = $renderer; + + return $this; + } + + /** + * Retrieve renderer instance + * + * @return null|Renderer\RendererInterface + */ + public function getRenderer() + { + return $this->renderer; + } + + /** + * Inject a helper instance with the registered renderer + * + * @param Helper\HelperInterface $helper + * @return void + */ + public function injectRenderer($helper) + { + $renderer = $this->getRenderer(); + if (null === $renderer) { + return; + } + $helper->setView($renderer); + } + + /** + * Inject a helper instance with the registered translator + * + * @param Helper\HelperInterface $helper + * @return void + */ + public function injectTranslator($helper) + { + if (!$helper instanceof TranslatorAwareInterface) { + return; + } + + $locator = $this->getServiceLocator(); + + if (!$locator) { + return; + } + + if ($locator->has('MvcTranslator')) { + $helper->setTranslator($locator->get('MvcTranslator')); + return; + } + + if ($locator->has('Zend\I18n\Translator\TranslatorInterface')) { + $helper->setTranslator($locator->get('Zend\I18n\Translator\TranslatorInterface')); + return; + } + + if ($locator->has('Translator')) { + $helper->setTranslator($locator->get('Translator')); + return; + } + } + + /** + * Validate the plugin + * + * Checks that the helper loaded is an instance of Helper\HelperInterface. + * + * @param mixed $plugin + * @return void + * @throws Exception\InvalidHelperException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof Helper\HelperInterface) { + // we're okay + return; + } + + throw new Exception\InvalidHelperException(sprintf( + 'Plugin of type %s is invalid; must implement %s\Helper\HelperInterface', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/library/Zend/View/Model/ClearableModelInterface.php b/library/Zend/View/Model/ClearableModelInterface.php new file mode 100755 index 0000000000..2edfe719e9 --- /dev/null +++ b/library/Zend/View/Model/ClearableModelInterface.php @@ -0,0 +1,23 @@ +options['errorLevel'] = $errorLevel; + } + + /** + * @return int + */ + public function getErrorLevel() + { + if (array_key_exists('errorLevel', $this->options)) { + return $this->options['errorLevel']; + } + } + + /** + * Set result text. + * + * @param string $text + * @return \Zend\View\Model\ConsoleModel + */ + public function setResult($text) + { + $this->setVariable(self::RESULT, $text); + return $this; + } + + /** + * Get result text. + * + * @return mixed + */ + public function getResult() + { + return $this->getVariable(self::RESULT); + } +} diff --git a/library/Zend/View/Model/FeedModel.php b/library/Zend/View/Model/FeedModel.php new file mode 100755 index 0000000000..fc8351cd04 --- /dev/null +++ b/library/Zend/View/Model/FeedModel.php @@ -0,0 +1,89 @@ +feed instanceof Feed) { + return $this->feed; + } + + if (!$this->type) { + $options = $this->getOptions(); + if (isset($options['feed_type'])) { + $this->type = $options['feed_type']; + } + } + + $variables = $this->getVariables(); + $feed = FeedFactory::factory($variables); + $this->setFeed($feed); + + return $this->feed; + } + + /** + * Set the feed object + * + * @param Feed $feed + * @return FeedModel + */ + public function setFeed(Feed $feed) + { + $this->feed = $feed; + return $this; + } + + /** + * Get the feed type + * + * @return false|string + */ + public function getFeedType() + { + if ($this->type) { + return $this->type; + } + + $options = $this->getOptions(); + if (isset($options['feed_type'])) { + $this->type = $options['feed_type']; + } + return $this->type; + } +} diff --git a/library/Zend/View/Model/JsonModel.php b/library/Zend/View/Model/JsonModel.php new file mode 100755 index 0000000000..191d3f5ec8 --- /dev/null +++ b/library/Zend/View/Model/JsonModel.php @@ -0,0 +1,69 @@ +jsonpCallback = $callback; + return $this; + } + + /** + * Serialize to JSON + * + * @return string + */ + public function serialize() + { + $variables = $this->getVariables(); + if ($variables instanceof Traversable) { + $variables = ArrayUtils::iteratorToArray($variables); + } + + if (null !== $this->jsonpCallback) { + return $this->jsonpCallback.'('.Json::encode($variables).');'; + } + return Json::encode($variables); + } +} diff --git a/library/Zend/View/Model/ModelInterface.php b/library/Zend/View/Model/ModelInterface.php new file mode 100755 index 0000000000..810540d703 --- /dev/null +++ b/library/Zend/View/Model/ModelInterface.php @@ -0,0 +1,167 @@ +setVariables($variables, true); + + if (null !== $options) { + $this->setOptions($options); + } + } + + /** + * Property overloading: set variable value + * + * @param string $name + * @param mixed $value + * @return void + */ + public function __set($name, $value) + { + $this->setVariable($name, $value); + } + + /** + * Property overloading: get variable value + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + if (!$this->__isset($name)) { + return null; + } + + $variables = $this->getVariables(); + return $variables[$name]; + } + + /** + * Property overloading: do we have the requested variable value? + * + * @param string $name + * @return bool + */ + public function __isset($name) + { + $variables = $this->getVariables(); + return isset($variables[$name]); + } + + /** + * Property overloading: unset the requested variable + * + * @param string $name + * @return void + */ + public function __unset($name) + { + if (!$this->__isset($name)) { + return null; + } + + unset($this->variables[$name]); + } + + /** + * Set a single option + * + * @param string $name + * @param mixed $value + * @return ViewModel + */ + public function setOption($name, $value) + { + $this->options[(string) $name] = $value; + return $this; + } + + /** + * Get a single option + * + * @param string $name The option to get. + * @param mixed|null $default (optional) A default value if the option is not yet set. + * @return mixed + */ + public function getOption($name, $default = null) + { + $name = (string) $name; + return array_key_exists($name, $this->options) ? $this->options[$name] : $default; + } + + /** + * Set renderer options/hints en masse + * + * @param array|Traversable $options + * @throws \Zend\View\Exception\InvalidArgumentException + * @return ViewModel + */ + public function setOptions($options) + { + // Assumption is that lowest common denominator for renderer configuration + // is an array + if ($options instanceof Traversable) { + $options = ArrayUtils::iteratorToArray($options); + } + + if (!is_array($options)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects an array, or Traversable argument; received "%s"', + __METHOD__, + (is_object($options) ? get_class($options) : gettype($options)) + )); + } + + $this->options = $options; + return $this; + } + + /** + * Get renderer options/hints + * + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * Clear any existing renderer options/hints + * + * @return ViewModel + */ + public function clearOptions() + { + $this->options = array(); + return $this; + } + + /** + * Get a single view variable + * + * @param string $name + * @param mixed|null $default (optional) default value if the variable is not present. + * @return mixed + */ + public function getVariable($name, $default = null) + { + $name = (string) $name; + if (array_key_exists($name, $this->variables)) { + return $this->variables[$name]; + } + + return $default; + } + + /** + * Set view variable + * + * @param string $name + * @param mixed $value + * @return ViewModel + */ + public function setVariable($name, $value) + { + $this->variables[(string) $name] = $value; + return $this; + } + + /** + * Set view variables en masse + * + * Can be an array or a Traversable + ArrayAccess object. + * + * @param array|ArrayAccess|Traversable $variables + * @param bool $overwrite Whether or not to overwrite the internal container with $variables + * @throws Exception\InvalidArgumentException + * @return ViewModel + */ + public function setVariables($variables, $overwrite = false) + { + if (!is_array($variables) && !$variables instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects an array, or Traversable argument; received "%s"', + __METHOD__, + (is_object($variables) ? get_class($variables) : gettype($variables)) + )); + } + + if ($overwrite) { + if (is_object($variables) && !$variables instanceof ArrayAccess) { + $variables = ArrayUtils::iteratorToArray($variables); + } + + $this->variables = $variables; + return $this; + } + + foreach ($variables as $key => $value) { + $this->setVariable($key, $value); + } + + return $this; + } + + /** + * Get view variables + * + * @return array|ArrayAccess|Traversable + */ + public function getVariables() + { + return $this->variables; + } + + /** + * Clear all variables + * + * Resets the internal variable container to an empty container. + * + * @return ViewModel + */ + public function clearVariables() + { + $this->variables = new ViewVariables(); + return $this; + } + + /** + * Set the template to be used by this model + * + * @param string $template + * @return ViewModel + */ + public function setTemplate($template) + { + $this->template = (string) $template; + return $this; + } + + /** + * Get the template to be used by this model + * + * @return string + */ + public function getTemplate() + { + return $this->template; + } + + /** + * Add a child model + * + * @param ModelInterface $child + * @param null|string $captureTo Optional; if specified, the "capture to" value to set on the child + * @param null|bool $append Optional; if specified, append to child with the same capture + * @return ViewModel + */ + public function addChild(ModelInterface $child, $captureTo = null, $append = null) + { + $this->children[] = $child; + if (null !== $captureTo) { + $child->setCaptureTo($captureTo); + } + if (null !== $append) { + $child->setAppend($append); + } + + return $this; + } + + /** + * Return all children. + * + * Return specifies an array, but may be any iterable object. + * + * @return array + */ + public function getChildren() + { + return $this->children; + } + + /** + * Does the model have any children? + * + * @return bool + */ + public function hasChildren() + { + return (0 < count($this->children)); + } + + /** + * Clears out all child models + * + * @return ViewModel + */ + public function clearChildren() + { + $this->children = array(); + return $this; + } + + /** + * Returns an array of Viewmodels with captureTo value $capture + * + * @param string $capture + * @param bool $recursive search recursive through children, default true + * @return array + */ + public function getChildrenByCaptureTo($capture, $recursive = true) + { + $children = array(); + + foreach ($this->children as $child) { + if ($recursive === true) { + $children += $child->getChildrenByCaptureTo($capture); + } + + if ($child->captureTo() === $capture) { + $children[] = $child; + } + } + + return $children; + } + + /** + * Set the name of the variable to capture this model to, if it is a child model + * + * @param string $capture + * @return ViewModel + */ + public function setCaptureTo($capture) + { + $this->captureTo = (string) $capture; + return $this; + } + + /** + * Get the name of the variable to which to capture this model + * + * @return string + */ + public function captureTo() + { + return $this->captureTo; + } + + /** + * Set flag indicating whether or not this is considered a terminal or standalone model + * + * @param bool $terminate + * @return ViewModel + */ + public function setTerminal($terminate) + { + $this->terminate = (bool) $terminate; + return $this; + } + + /** + * Is this considered a terminal or standalone model? + * + * @return bool + */ + public function terminate() + { + return $this->terminate; + } + + /** + * Set flag indicating whether or not append to child with the same capture + * + * @param bool $append + * @return ViewModel + */ + public function setAppend($append) + { + $this->append = (bool) $append; + return $this; + } + + /** + * Is this append to child with the same capture? + * + * @return bool + */ + public function isAppend() + { + return $this->append; + } + + /** + * Return count of children + * + * @return int + */ + public function count() + { + return count($this->children); + } + + /** + * Get iterator of children + * + * @return ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->children); + } +} diff --git a/library/Zend/View/README.md b/library/Zend/View/README.md new file mode 100755 index 0000000000..ec19b2c016 --- /dev/null +++ b/library/Zend/View/README.md @@ -0,0 +1,15 @@ +View Component from ZF2 +======================= + +This is the View component for ZF2. + +- File issues at https://github.com/zendframework/zf2/issues +- Create pull requests against https://github.com/zendframework/zf2 +- Documentation is at http://framework.zend.com/docs + +LICENSE +------- + +The files in this archive are released under the [Zend Framework +license](http://framework.zend.com/license), which is a 3-clause BSD license. + diff --git a/library/Zend/View/Renderer/ConsoleRenderer.php b/library/Zend/View/Renderer/ConsoleRenderer.php new file mode 100755 index 0000000000..5f0cf10b26 --- /dev/null +++ b/library/Zend/View/Renderer/ConsoleRenderer.php @@ -0,0 +1,149 @@ +init(); + } + + public function setResolver(ResolverInterface $resolver) + { + return $this; + } + + /** + * Return the template engine object + * + * Returns the object instance, as it is its own template engine + * + * @return PhpRenderer + */ + public function getEngine() + { + return $this; + } + + /** + * Allow custom object initialization when extending ConsoleRenderer + * + * Triggered by {@link __construct() the constructor} as its final action. + * + * @return void + */ + public function init() + { + } + + /** + * Set filter chain + * + * @param FilterChain $filters + * @return ConsoleRenderer + */ + public function setFilterChain(FilterChain $filters) + { + $this->__filterChain = $filters; + return $this; + } + + /** + * Retrieve filter chain for post-filtering script content + * + * @return FilterChain + */ + public function getFilterChain() + { + if (null === $this->__filterChain) { + $this->setFilterChain(new FilterChain()); + } + return $this->__filterChain; + } + + /** + * Recursively processes all ViewModels and returns output. + * + * @param string|ModelInterface $model A ViewModel instance. + * @param null|array|\Traversable $values Values to use when rendering. If none + * provided, uses those in the composed + * variables container. + * @return string Console output. + */ + public function render($model, $values = null) + { + if (!$model instanceof ModelInterface) { + return ''; + } + + $result = ''; + $options = $model->getOptions(); + foreach ($options as $setting => $value) { + $method = 'set' . $setting; + if (method_exists($this, $method)) { + $this->$method($value); + } + unset($method, $setting, $value); + } + unset($options); + + $values = $model->getVariables(); + + if (isset($values['result'])) { + // filter and append the result + $result .= $this->getFilterChain()->filter($values['result']); + } + + if ($model->hasChildren()) { + // recursively render all children + foreach ($model->getChildren() as $child) { + $result .= $this->render($child, $values); + } + } + + return $result; + } + + /** + * @see Zend\View\Renderer\TreeRendererInterface + * @return bool + */ + public function canRenderTrees() + { + return true; + } +} diff --git a/library/Zend/View/Renderer/FeedRenderer.php b/library/Zend/View/Renderer/FeedRenderer.php new file mode 100755 index 0000000000..b91964683e --- /dev/null +++ b/library/Zend/View/Renderer/FeedRenderer.php @@ -0,0 +1,138 @@ +resolver = $resolver; + } + + /** + * Renders values as JSON + * + * @todo Determine what use case exists for accepting only $nameOrModel + * @param string|Model $nameOrModel The script/resource process, or a view model + * @param null|array|\ArrayAccess $values Values to use during rendering + * @throws Exception\InvalidArgumentException + * @return string The script output. + */ + public function render($nameOrModel, $values = null) + { + if ($nameOrModel instanceof Model) { + // Use case 1: View Model provided + // Non-FeedModel: cast to FeedModel + if (!$nameOrModel instanceof FeedModel) { + $vars = $nameOrModel->getVariables(); + $options = $nameOrModel->getOptions(); + $type = $this->getFeedType(); + if (isset($options['feed_type'])) { + $type = $options['feed_type']; + } else { + $this->setFeedType($type); + } + $nameOrModel = new FeedModel($vars, array('feed_type' => $type)); + } + } elseif (is_string($nameOrModel)) { + // Use case 2: string $nameOrModel + array|Traversable|Feed $values + $nameOrModel = new FeedModel($values, (array) $nameOrModel); + } else { + // Use case 3: failure + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a ViewModel or a string feed type as the first argument; received "%s"', + __METHOD__, + (is_object($nameOrModel) ? get_class($nameOrModel) : gettype($nameOrModel)) + )); + } + + // Get feed and type + $feed = $nameOrModel->getFeed(); + $type = $nameOrModel->getFeedType(); + if (!$type) { + $type = $this->getFeedType(); + } else { + $this->setFeedType($type); + } + + // Render feed + return $feed->export($type); + } + + /** + * Set feed type ('rss' or 'atom') + * + * @param string $feedType + * @throws Exception\InvalidArgumentException + * @return FeedRenderer + */ + public function setFeedType($feedType) + { + $feedType = strtolower($feedType); + if (!in_array($feedType, array('rss', 'atom'))) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a string of either "rss" or "atom"', + __METHOD__ + )); + } + + $this->feedType = $feedType; + return $this; + } + + /** + * Get feed type + * + * @return string + */ + public function getFeedType() + { + return $this->feedType; + } +} diff --git a/library/Zend/View/Renderer/JsonRenderer.php b/library/Zend/View/Renderer/JsonRenderer.php new file mode 100755 index 0000000000..9666261db8 --- /dev/null +++ b/library/Zend/View/Renderer/JsonRenderer.php @@ -0,0 +1,243 @@ +resolver = $resolver; + } + + /** + * Set flag indicating whether or not to merge unnamed children + * + * @param bool $mergeUnnamedChildren + * @return JsonRenderer + */ + public function setMergeUnnamedChildren($mergeUnnamedChildren) + { + $this->mergeUnnamedChildren = (bool) $mergeUnnamedChildren; + return $this; + } + + /** + * Set the JSONP callback function name + * + * @param string $callback + * @return JsonRenderer + */ + public function setJsonpCallback($callback) + { + $callback = (string) $callback; + if (!empty($callback)) { + $this->jsonpCallback = $callback; + } + return $this; + } + + /** + * Returns whether or not the jsonpCallback has been set + * + * @return bool + */ + public function hasJsonpCallback() + { + return (null !== $this->jsonpCallback); + } + + /** + * Should we merge unnamed children? + * + * @return bool + */ + public function mergeUnnamedChildren() + { + return $this->mergeUnnamedChildren; + } + + /** + * Renders values as JSON + * + * @todo Determine what use case exists for accepting both $nameOrModel and $values + * @param string|Model $nameOrModel The script/resource process, or a view model + * @param null|array|\ArrayAccess $values Values to use during rendering + * @throws Exception\DomainException + * @return string The script output. + */ + public function render($nameOrModel, $values = null) + { + // use case 1: View Models + // Serialize variables in view model + if ($nameOrModel instanceof Model) { + if ($nameOrModel instanceof JsonModel) { + $children = $this->recurseModel($nameOrModel, false); + $this->injectChildren($nameOrModel, $children); + $values = $nameOrModel->serialize(); + } else { + $values = $this->recurseModel($nameOrModel); + $values = Json::encode($values); + } + + if ($this->hasJsonpCallback()) { + $values = $this->jsonpCallback . '(' . $values . ');'; + } + return $values; + } + + // use case 2: $nameOrModel is populated, $values is not + // Serialize $nameOrModel + if (null === $values) { + if (!is_object($nameOrModel) || $nameOrModel instanceof JsonSerializable) { + $return = Json::encode($nameOrModel); + } elseif ($nameOrModel instanceof Traversable) { + $nameOrModel = ArrayUtils::iteratorToArray($nameOrModel); + $return = Json::encode($nameOrModel); + } else { + $return = Json::encode(get_object_vars($nameOrModel)); + } + + if ($this->hasJsonpCallback()) { + $return = $this->jsonpCallback . '(' . $return . ');'; + } + return $return; + } + + // use case 3: Both $nameOrModel and $values are populated + throw new Exception\DomainException(sprintf( + '%s: Do not know how to handle operation when both $nameOrModel and $values are populated', + __METHOD__ + )); + } + + /** + * Can this renderer render trees of view models? + * + * Yes. + * + * @return true + */ + public function canRenderTrees() + { + return true; + } + + /** + * Retrieve values from a model and recurse its children to build a data structure + * + * @param Model $model + * @param bool $mergeWithVariables Whether or not to merge children with + * the variables of the $model + * @return array + */ + protected function recurseModel(Model $model, $mergeWithVariables = true) + { + $values = array(); + if ($mergeWithVariables) { + $values = $model->getVariables(); + } + + if ($values instanceof Traversable) { + $values = ArrayUtils::iteratorToArray($values); + } + + if (!$model->hasChildren()) { + return $values; + } + + $mergeChildren = $this->mergeUnnamedChildren(); + foreach ($model as $child) { + $captureTo = $child->captureTo(); + if (!$captureTo && !$mergeChildren) { + // We don't want to do anything with this child + continue; + } + + $childValues = $this->recurseModel($child); + if ($captureTo) { + // Capturing to a specific key + // TODO please complete if append is true. must change old + // value to array and append to array? + $values[$captureTo] = $childValues; + } elseif ($mergeChildren) { + // Merging values with parent + $values = array_replace_recursive($values, $childValues); + } + } + return $values; + } + + /** + * Inject discovered child model values into parent model + * + * @todo detect collisions and decide whether to append and/or aggregate? + * @param Model $model + * @param array $children + */ + protected function injectChildren(Model $model, array $children) + { + foreach ($children as $child => $value) { + // TODO detect collisions and decide whether to append and/or aggregate? + $model->setVariable($child, $value); + } + } +} diff --git a/library/Zend/View/Renderer/PhpRenderer.php b/library/Zend/View/Renderer/PhpRenderer.php new file mode 100755 index 0000000000..87baf3d988 --- /dev/null +++ b/library/Zend/View/Renderer/PhpRenderer.php @@ -0,0 +1,574 @@ +init(); + } + + /** + * Return the template engine object + * + * Returns the object instance, as it is its own template engine + * + * @return PhpRenderer + */ + public function getEngine() + { + return $this; + } + + /** + * Allow custom object initialization when extending PhpRenderer + * + * Triggered by {@link __construct() the constructor} as its final action. + * + * @return void + */ + public function init() + { + } + + /** + * Set script resolver + * + * @param Resolver $resolver + * @return PhpRenderer + * @throws Exception\InvalidArgumentException + */ + public function setResolver(Resolver $resolver) + { + $this->__templateResolver = $resolver; + return $this; + } + + /** + * Retrieve template name or template resolver + * + * @param null|string $name + * @return string|Resolver + */ + public function resolver($name = null) + { + if (null === $this->__templateResolver) { + $this->setResolver(new TemplatePathStack()); + } + + if (null !== $name) { + return $this->__templateResolver->resolve($name, $this); + } + + return $this->__templateResolver; + } + + /** + * Set variable storage + * + * Expects either an array, or an object implementing ArrayAccess. + * + * @param array|ArrayAccess $variables + * @return PhpRenderer + * @throws Exception\InvalidArgumentException + */ + public function setVars($variables) + { + if (!is_array($variables) && !$variables instanceof ArrayAccess) { + throw new Exception\InvalidArgumentException(sprintf( + 'Expected array or ArrayAccess object; received "%s"', + (is_object($variables) ? get_class($variables) : gettype($variables)) + )); + } + + // Enforce a Variables container + if (!$variables instanceof Variables) { + $variablesAsArray = array(); + foreach ($variables as $key => $value) { + $variablesAsArray[$key] = $value; + } + $variables = new Variables($variablesAsArray); + } + + $this->__vars = $variables; + return $this; + } + + /** + * Get a single variable, or all variables + * + * @param mixed $key + * @return mixed + */ + public function vars($key = null) + { + if (null === $this->__vars) { + $this->setVars(new Variables()); + } + + if (null === $key) { + return $this->__vars; + } + return $this->__vars[$key]; + } + + /** + * Get a single variable + * + * @param mixed $key + * @return mixed + */ + public function get($key) + { + if (null === $this->__vars) { + $this->setVars(new Variables()); + } + + return $this->__vars[$key]; + } + + /** + * Overloading: proxy to Variables container + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + $vars = $this->vars(); + return $vars[$name]; + } + + /** + * Overloading: proxy to Variables container + * + * @param string $name + * @param mixed $value + * @return void + */ + public function __set($name, $value) + { + $vars = $this->vars(); + $vars[$name] = $value; + } + + /** + * Overloading: proxy to Variables container + * + * @param string $name + * @return bool + */ + public function __isset($name) + { + $vars = $this->vars(); + return isset($vars[$name]); + } + + /** + * Overloading: proxy to Variables container + * + * @param string $name + * @return void + */ + public function __unset($name) + { + $vars = $this->vars(); + if (!isset($vars[$name])) { + return; + } + unset($vars[$name]); + } + + /** + * Set helper plugin manager instance + * + * @param string|HelperPluginManager $helpers + * @return PhpRenderer + * @throws Exception\InvalidArgumentException + */ + public function setHelperPluginManager($helpers) + { + if (is_string($helpers)) { + if (!class_exists($helpers)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid helper helpers class provided (%s)', + $helpers + )); + } + $helpers = new $helpers(); + } + if (!$helpers instanceof HelperPluginManager) { + throw new Exception\InvalidArgumentException(sprintf( + 'Helper helpers must extend Zend\View\HelperPluginManager; got type "%s" instead', + (is_object($helpers) ? get_class($helpers) : gettype($helpers)) + )); + } + $helpers->setRenderer($this); + $this->__helpers = $helpers; + + return $this; + } + + /** + * Get helper plugin manager instance + * + * @return HelperPluginManager + */ + public function getHelperPluginManager() + { + if (null === $this->__helpers) { + $this->setHelperPluginManager(new HelperPluginManager()); + } + return $this->__helpers; + } + + /** + * Get plugin instance + * + * @param string $name Name of plugin to return + * @param null|array $options Options to pass to plugin constructor (if not already instantiated) + * @return AbstractHelper + */ + public function plugin($name, array $options = null) + { + return $this->getHelperPluginManager()->get($name, $options); + } + + /** + * Overloading: proxy to helpers + * + * Proxies to the attached plugin manager to retrieve, return, and potentially + * execute helpers. + * + * * If the helper does not define __invoke, it will be returned + * * If the helper does define __invoke, it will be called as a functor + * + * @param string $method + * @param array $argv + * @return mixed + */ + public function __call($method, $argv) + { + if (!isset($this->__pluginCache[$method])) { + $this->__pluginCache[$method] = $this->plugin($method); + } + if (is_callable($this->__pluginCache[$method])) { + return call_user_func_array($this->__pluginCache[$method], $argv); + } + return $this->__pluginCache[$method]; + } + + /** + * Set filter chain + * + * @param FilterChain $filters + * @return PhpRenderer + */ + public function setFilterChain(FilterChain $filters) + { + $this->__filterChain = $filters; + return $this; + } + + /** + * Retrieve filter chain for post-filtering script content + * + * @return FilterChain + */ + public function getFilterChain() + { + if (null === $this->__filterChain) { + $this->setFilterChain(new FilterChain()); + } + return $this->__filterChain; + } + + /** + * Processes a view script and returns the output. + * + * @param string|Model $nameOrModel Either the template to use, or a + * ViewModel. The ViewModel must have the + * template as an option in order to be + * valid. + * @param null|array|Traversable $values Values to use when rendering. If none + * provided, uses those in the composed + * variables container. + * @return string The script output. + * @throws Exception\DomainException if a ViewModel is passed, but does not + * contain a template option. + * @throws Exception\InvalidArgumentException if the values passed are not + * an array or ArrayAccess object + * @throws Exception\RuntimeException if the template cannot be rendered + */ + public function render($nameOrModel, $values = null) + { + if ($nameOrModel instanceof Model) { + $model = $nameOrModel; + $nameOrModel = $model->getTemplate(); + if (empty($nameOrModel)) { + throw new Exception\DomainException(sprintf( + '%s: received View Model argument, but template is empty', + __METHOD__ + )); + } + $options = $model->getOptions(); + foreach ($options as $setting => $value) { + $method = 'set' . $setting; + if (method_exists($this, $method)) { + $this->$method($value); + } + unset($method, $setting, $value); + } + unset($options); + + // Give view model awareness via ViewModel helper + $helper = $this->plugin('view_model'); + $helper->setCurrent($model); + + $values = $model->getVariables(); + unset($model); + } + + // find the script file name using the parent private method + $this->addTemplate($nameOrModel); + unset($nameOrModel); // remove $name from local scope + + $this->__varsCache[] = $this->vars(); + + if (null !== $values) { + $this->setVars($values); + } + unset($values); + + // extract all assigned vars (pre-escaped), but not 'this'. + // assigns to a double-underscored variable, to prevent naming collisions + $__vars = $this->vars()->getArrayCopy(); + if (array_key_exists('this', $__vars)) { + unset($__vars['this']); + } + extract($__vars); + unset($__vars); // remove $__vars from local scope + + while ($this->__template = array_pop($this->__templates)) { + $this->__file = $this->resolver($this->__template); + if (!$this->__file) { + throw new Exception\RuntimeException(sprintf( + '%s: Unable to render template "%s"; resolver could not resolve to a file', + __METHOD__, + $this->__template + )); + } + try { + ob_start(); + $includeReturn = include $this->__file; + $this->__content = ob_get_clean(); + } catch (\Exception $ex) { + ob_end_clean(); + throw $ex; + } + if ($includeReturn === false && empty($this->__content)) { + throw new Exception\UnexpectedValueException(sprintf( + '%s: Unable to render template "%s"; file include failed', + __METHOD__, + $this->__file + )); + } + } + + $this->setVars(array_pop($this->__varsCache)); + + return $this->getFilterChain()->filter($this->__content); // filter output + } + + /** + * Set flag indicating whether or not we should render trees of view models + * + * If set to true, the View instance will not attempt to render children + * separately, but instead pass the root view model directly to the PhpRenderer. + * It is then up to the developer to render the children from within the + * view script. + * + * @param bool $renderTrees + * @return PhpRenderer + */ + public function setCanRenderTrees($renderTrees) + { + $this->__renderTrees = (bool) $renderTrees; + return $this; + } + + /** + * Can we render trees, or are we configured to do so? + * + * @return bool + */ + public function canRenderTrees() + { + return $this->__renderTrees; + } + + /** + * Add a template to the stack + * + * @param string $template + * @return PhpRenderer + */ + public function addTemplate($template) + { + $this->__templates[] = $template; + return $this; + } + + /** + * Make sure View variables are cloned when the view is cloned. + * + * @return PhpRenderer + */ + public function __clone() + { + $this->__vars = clone $this->vars(); + } +} diff --git a/library/Zend/View/Renderer/RendererInterface.php b/library/Zend/View/Renderer/RendererInterface.php new file mode 100755 index 0000000000..d3dfa77f7c --- /dev/null +++ b/library/Zend/View/Renderer/RendererInterface.php @@ -0,0 +1,47 @@ +queue = new PriorityQueue(); + } + + /** + * Return count of attached resolvers + * + * @return int + */ + public function count() + { + return $this->queue->count(); + } + + /** + * IteratorAggregate: return internal iterator + * + * @return PriorityQueue + */ + public function getIterator() + { + return $this->queue; + } + + /** + * Attach a resolver + * + * @param Resolver $resolver + * @param int $priority + * @return AggregateResolver + */ + public function attach(Resolver $resolver, $priority = 1) + { + $this->queue->insert($resolver, $priority); + return $this; + } + + /** + * Resolve a template/pattern name to a resource the renderer can consume + * + * @param string $name + * @param null|Renderer $renderer + * @return false|string + */ + public function resolve($name, Renderer $renderer = null) + { + $this->lastLookupFailure = false; + $this->lastSuccessfulResolver = null; + + if (0 === count($this->queue)) { + $this->lastLookupFailure = static::FAILURE_NO_RESOLVERS; + return false; + } + + foreach ($this->queue as $resolver) { + $resource = $resolver->resolve($name, $renderer); + if (!$resource) { + // No resource found; try next resolver + continue; + } + + // Resource found; return it + $this->lastSuccessfulResolver = $resolver; + return $resource; + } + + $this->lastLookupFailure = static::FAILURE_NOT_FOUND; + return false; + } + + /** + * Return the last successful resolver, if any + * + * @return Resolver + */ + public function getLastSuccessfulResolver() + { + return $this->lastSuccessfulResolver; + } + + /** + * Get last lookup failure + * + * @return false|string + */ + public function getLastLookupFailure() + { + return $this->lastLookupFailure; + } +} diff --git a/library/Zend/View/Resolver/ResolverInterface.php b/library/Zend/View/Resolver/ResolverInterface.php new file mode 100755 index 0000000000..8ffe130c66 --- /dev/null +++ b/library/Zend/View/Resolver/ResolverInterface.php @@ -0,0 +1,24 @@ +setMap($map); + } + + /** + * IteratorAggregate: return internal iterator + * + * @return Traversable + */ + public function getIterator() + { + return new ArrayIterator($this->map); + } + + /** + * Set (overwrite) template map + * + * Maps should be arrays or Traversable objects with name => path pairs + * + * @param array|Traversable $map + * @throws Exception\InvalidArgumentException + * @return TemplateMapResolver + */ + public function setMap($map) + { + if (!is_array($map) && !$map instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects an array or Traversable, received "%s"', + __METHOD__, + (is_object($map) ? get_class($map) : gettype($map)) + )); + } + + if ($map instanceof Traversable) { + $map = ArrayUtils::iteratorToArray($map); + } + + $this->map = $map; + return $this; + } + + /** + * Add an entry to the map + * + * @param string|array|Traversable $nameOrMap + * @param null|string $path + * @throws Exception\InvalidArgumentException + * @return TemplateMapResolver + */ + public function add($nameOrMap, $path = null) + { + if (is_array($nameOrMap) || $nameOrMap instanceof Traversable) { + $this->merge($nameOrMap); + return $this; + } + + if (!is_string($nameOrMap)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects a string, array, or Traversable for the first argument; received "%s"', + __METHOD__, + (is_object($nameOrMap) ? get_class($nameOrMap) : gettype($nameOrMap)) + )); + } + + if (empty($path)) { + if (isset($this->map[$nameOrMap])) { + unset($this->map[$nameOrMap]); + } + return $this; + } + + $this->map[$nameOrMap] = $path; + return $this; + } + + /** + * Merge internal map with provided map + * + * @param array|Traversable $map + * @throws Exception\InvalidArgumentException + * @return TemplateMapResolver + */ + public function merge($map) + { + if (!is_array($map) && !$map instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + '%s: expects an array or Traversable, received "%s"', + __METHOD__, + (is_object($map) ? get_class($map) : gettype($map)) + )); + } + + if ($map instanceof Traversable) { + $map = ArrayUtils::iteratorToArray($map); + } + + $this->map = array_replace_recursive($this->map, $map); + return $this; + } + + /** + * Does the resolver contain an entry for the given name? + * + * @param string $name + * @return bool + */ + public function has($name) + { + return array_key_exists($name, $this->map); + } + + /** + * Retrieve a template path by name + * + * @param string $name + * @return false|string + * @throws Exception\DomainException if no entry exists + */ + public function get($name) + { + if (!$this->has($name)) { + return false; + } + return $this->map[$name]; + } + + /** + * Retrieve the template map + * + * @return array + */ + public function getMap() + { + return $this->map; + } + + /** + * Resolve a template/pattern name to a resource the renderer can consume + * + * @param string $name + * @param null|Renderer $renderer + * @return string + */ + public function resolve($name, Renderer $renderer = null) + { + return $this->get($name); + } +} diff --git a/library/Zend/View/Resolver/TemplatePathStack.php b/library/Zend/View/Resolver/TemplatePathStack.php new file mode 100755 index 0000000000..359be18f34 --- /dev/null +++ b/library/Zend/View/Resolver/TemplatePathStack.php @@ -0,0 +1,338 @@ +useViewStream = (bool) ini_get('short_open_tag'); + if ($this->useViewStream) { + if (!in_array('zend.view', stream_get_wrappers())) { + stream_wrapper_register('zend.view', 'Zend\View\Stream'); + } + } + + $this->paths = new SplStack; + if (null !== $options) { + $this->setOptions($options); + } + } + + /** + * Configure object + * + * @param array|Traversable $options + * @return void + * @throws Exception\InvalidArgumentException + */ + public function setOptions($options) + { + if (!is_array($options) && !$options instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + 'Expected array or Traversable object; received "%s"', + (is_object($options) ? get_class($options) : gettype($options)) + )); + } + + foreach ($options as $key => $value) { + switch (strtolower($key)) { + case 'lfi_protection': + $this->setLfiProtection($value); + break; + case 'script_paths': + $this->addPaths($value); + break; + case 'use_stream_wrapper': + $this->setUseStreamWrapper($value); + break; + case 'default_suffix': + $this->setDefaultSuffix($value); + break; + default: + break; + } + } + } + + /** + * Set default file suffix + * + * @param string $defaultSuffix + * @return TemplatePathStack + */ + public function setDefaultSuffix($defaultSuffix) + { + $this->defaultSuffix = (string) $defaultSuffix; + $this->defaultSuffix = ltrim($this->defaultSuffix, '.'); + return $this; + } + + /** + * Get default file suffix + * + * @return string + */ + public function getDefaultSuffix() + { + return $this->defaultSuffix; + } + + /** + * Add many paths to the stack at once + * + * @param array $paths + * @return TemplatePathStack + */ + public function addPaths(array $paths) + { + foreach ($paths as $path) { + $this->addPath($path); + } + return $this; + } + + /** + * Rest the path stack to the paths provided + * + * @param SplStack|array $paths + * @return TemplatePathStack + * @throws Exception\InvalidArgumentException + */ + public function setPaths($paths) + { + if ($paths instanceof SplStack) { + $this->paths = $paths; + } elseif (is_array($paths)) { + $this->clearPaths(); + $this->addPaths($paths); + } else { + throw new Exception\InvalidArgumentException( + "Invalid argument provided for \$paths, expecting either an array or SplStack object" + ); + } + + return $this; + } + + /** + * Normalize a path for insertion in the stack + * + * @param string $path + * @return string + */ + public static function normalizePath($path) + { + $path = rtrim($path, '/'); + $path = rtrim($path, '\\'); + $path .= DIRECTORY_SEPARATOR; + return $path; + } + + /** + * Add a single path to the stack + * + * @param string $path + * @return TemplatePathStack + * @throws Exception\InvalidArgumentException + */ + public function addPath($path) + { + if (!is_string($path)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid path provided; must be a string, received %s', + gettype($path) + )); + } + $this->paths[] = static::normalizePath($path); + return $this; + } + + /** + * Clear all paths + * + * @return void + */ + public function clearPaths() + { + $this->paths = new SplStack; + } + + /** + * Returns stack of paths + * + * @return SplStack + */ + public function getPaths() + { + return $this->paths; + } + + /** + * Set LFI protection flag + * + * @param bool $flag + * @return TemplatePathStack + */ + public function setLfiProtection($flag) + { + $this->lfiProtectionOn = (bool) $flag; + return $this; + } + + /** + * Return status of LFI protection flag + * + * @return bool + */ + public function isLfiProtectionOn() + { + return $this->lfiProtectionOn; + } + + /** + * Set flag indicating if stream wrapper should be used if short_open_tag is off + * + * @param bool $flag + * @return TemplatePathStack + */ + public function setUseStreamWrapper($flag) + { + $this->useStreamWrapper = (bool) $flag; + return $this; + } + + /** + * Should the stream wrapper be used if short_open_tag is off? + * + * Returns true if the use_stream_wrapper flag is set, and if short_open_tag + * is disabled. + * + * @return bool + */ + public function useStreamWrapper() + { + return ($this->useViewStream && $this->useStreamWrapper); + } + + /** + * Retrieve the filesystem path to a view script + * + * @param string $name + * @param null|Renderer $renderer + * @return string + * @throws Exception\DomainException + */ + public function resolve($name, Renderer $renderer = null) + { + $this->lastLookupFailure = false; + + if ($this->isLfiProtectionOn() && preg_match('#\.\.[\\\/]#', $name)) { + throw new Exception\DomainException( + 'Requested scripts may not include parent directory traversal ("../", "..\\" notation)' + ); + } + + if (!count($this->paths)) { + $this->lastLookupFailure = static::FAILURE_NO_PATHS; + return false; + } + + // Ensure we have the expected file extension + $defaultSuffix = $this->getDefaultSuffix(); + if (pathinfo($name, PATHINFO_EXTENSION) == '') { + $name .= '.' . $defaultSuffix; + } + + foreach ($this->paths as $path) { + $file = new SplFileInfo($path . $name); + if ($file->isReadable()) { + // Found! Return it. + if (($filePath = $file->getRealPath()) === false && substr($path, 0, 7) === 'phar://') { + // Do not try to expand phar paths (realpath + phars == fail) + $filePath = $path . $name; + if (!file_exists($filePath)) { + break; + } + } + if ($this->useStreamWrapper()) { + // If using a stream wrapper, prepend the spec to the path + $filePath = 'zend.view://' . $filePath; + } + return $filePath; + } + } + + $this->lastLookupFailure = static::FAILURE_NOT_FOUND; + return false; + } + + /** + * Get the last lookup failure message, if any + * + * @return false|string + */ + public function getLastLookupFailure() + { + return $this->lastLookupFailure; + } +} diff --git a/library/Zend/View/Strategy/FeedStrategy.php b/library/Zend/View/Strategy/FeedStrategy.php new file mode 100755 index 0000000000..40140da5a6 --- /dev/null +++ b/library/Zend/View/Strategy/FeedStrategy.php @@ -0,0 +1,111 @@ +renderer = $renderer; + } + + /** + * {@inheritDoc} + */ + public function attach(EventManagerInterface $events, $priority = 1) + { + $this->listeners[] = $events->attach(ViewEvent::EVENT_RENDERER, array($this, 'selectRenderer'), $priority); + $this->listeners[] = $events->attach(ViewEvent::EVENT_RESPONSE, array($this, 'injectResponse'), $priority); + } + + /** + * Detect if we should use the FeedRenderer based on model type and/or + * Accept header + * + * @param ViewEvent $e + * @return null|FeedRenderer + */ + public function selectRenderer(ViewEvent $e) + { + $model = $e->getModel(); + + if (!$model instanceof Model\FeedModel) { + // no FeedModel present; do nothing + return; + } + + // FeedModel found + return $this->renderer; + } + + /** + * Inject the response with the feed payload and appropriate Content-Type header + * + * @param ViewEvent $e + * @return void + */ + public function injectResponse(ViewEvent $e) + { + $renderer = $e->getRenderer(); + if ($renderer !== $this->renderer) { + // Discovered renderer is not ours; do nothing + return; + } + + $result = $e->getResult(); + if (!is_string($result) && !$result instanceof Feed) { + // We don't have a string, and thus, no feed + return; + } + + // If the result is a feed, export it + if ($result instanceof Feed) { + $result = $result->export($renderer->getFeedType()); + } + + // Get the content-type header based on feed type + $feedType = $renderer->getFeedType(); + $feedType = ('rss' == $feedType) + ? 'application/rss+xml' + : 'application/atom+xml'; + + $model = $e->getModel(); + $charset = ''; + + if ($model instanceof Model\FeedModel) { + $feed = $model->getFeed(); + + $charset = '; charset=' . $feed->getEncoding() . ';'; + } + + // Populate response + $response = $e->getResponse(); + $response->setContent($result); + $headers = $response->getHeaders(); + $headers->addHeaderLine('content-type', $feedType . $charset); + } +} diff --git a/library/Zend/View/Strategy/JsonStrategy.php b/library/Zend/View/Strategy/JsonStrategy.php new file mode 100755 index 0000000000..7cf4b91f09 --- /dev/null +++ b/library/Zend/View/Strategy/JsonStrategy.php @@ -0,0 +1,141 @@ +renderer = $renderer; + } + + /** + * {@inheritDoc} + */ + public function attach(EventManagerInterface $events, $priority = 1) + { + $this->listeners[] = $events->attach(ViewEvent::EVENT_RENDERER, array($this, 'selectRenderer'), $priority); + $this->listeners[] = $events->attach(ViewEvent::EVENT_RESPONSE, array($this, 'injectResponse'), $priority); + } + + /** + * Set the content-type character set + * + * @param string $charset + * @return JsonStrategy + */ + public function setCharset($charset) + { + $this->charset = (string) $charset; + return $this; + } + + /** + * Retrieve the current character set + * + * @return string + */ + public function getCharset() + { + return $this->charset; + } + + /** + * Detect if we should use the JsonRenderer based on model type and/or + * Accept header + * + * @param ViewEvent $e + * @return null|JsonRenderer + */ + public function selectRenderer(ViewEvent $e) + { + $model = $e->getModel(); + + if (!$model instanceof Model\JsonModel) { + // no JsonModel; do nothing + return; + } + + // JsonModel found + return $this->renderer; + } + + /** + * Inject the response with the JSON payload and appropriate Content-Type header + * + * @param ViewEvent $e + * @return void + */ + public function injectResponse(ViewEvent $e) + { + $renderer = $e->getRenderer(); + if ($renderer !== $this->renderer) { + // Discovered renderer is not ours; do nothing + return; + } + + $result = $e->getResult(); + if (!is_string($result)) { + // We don't have a string, and thus, no JSON + return; + } + + // Populate response + $response = $e->getResponse(); + $response->setContent($result); + $headers = $response->getHeaders(); + + if ($this->renderer->hasJsonpCallback()) { + $contentType = 'application/javascript'; + } else { + $contentType = 'application/json'; + } + + $contentType .= '; charset=' . $this->charset; + $headers->addHeaderLine('content-type', $contentType); + + if (in_array(strtoupper($this->charset), $this->multibyteCharsets)) { + $headers->addHeaderLine('content-transfer-encoding', 'BINARY'); + } + } +} diff --git a/library/Zend/View/Strategy/PhpRendererStrategy.php b/library/Zend/View/Strategy/PhpRendererStrategy.php new file mode 100755 index 0000000000..e8e04ceb3a --- /dev/null +++ b/library/Zend/View/Strategy/PhpRendererStrategy.php @@ -0,0 +1,127 @@ +renderer = $renderer; + } + + /** + * Retrieve the composed renderer + * + * @return PhpRenderer + */ + public function getRenderer() + { + return $this->renderer; + } + + /** + * Set list of possible content placeholders + * + * @param array $contentPlaceholders + * @return PhpRendererStrategy + */ + public function setContentPlaceholders(array $contentPlaceholders) + { + $this->contentPlaceholders = $contentPlaceholders; + return $this; + } + + /** + * Get list of possible content placeholders + * + * @return array + */ + public function getContentPlaceholders() + { + return $this->contentPlaceholders; + } + + /** + * {@inheritDoc} + */ + public function attach(EventManagerInterface $events, $priority = 1) + { + $this->listeners[] = $events->attach(ViewEvent::EVENT_RENDERER, array($this, 'selectRenderer'), $priority); + $this->listeners[] = $events->attach(ViewEvent::EVENT_RESPONSE, array($this, 'injectResponse'), $priority); + } + + /** + * Select the PhpRenderer; typically, this will be registered last or at + * low priority. + * + * @param ViewEvent $e + * @return PhpRenderer + */ + public function selectRenderer(ViewEvent $e) + { + return $this->renderer; + } + + /** + * Populate the response object from the View + * + * Populates the content of the response object from the view rendering + * results. + * + * @param ViewEvent $e + * @return void + */ + public function injectResponse(ViewEvent $e) + { + $renderer = $e->getRenderer(); + if ($renderer !== $this->renderer) { + return; + } + + $result = $e->getResult(); + $response = $e->getResponse(); + + // Set content + // If content is empty, check common placeholders to determine if they are + // populated, and set the content from them. + if (empty($result)) { + $placeholders = $renderer->plugin('placeholder'); + foreach ($this->contentPlaceholders as $placeholder) { + if ($placeholders->containerExists($placeholder)) { + $result = (string) $placeholders->getContainer($placeholder); + break; + } + } + } + $response->setContent($result); + } +} diff --git a/library/Zend/View/Stream.php b/library/Zend/View/Stream.php new file mode 100755 index 0000000000..42bb7a962d --- /dev/null +++ b/library/Zend/View/Stream.php @@ -0,0 +1,183 @@ +data = file_get_contents($path); + + /** + * If reading the file failed, update our local stat store + * to reflect the real stat of the file, then return on failure + */ + if ($this->data === false) { + $this->stat = stat($path); + return false; + } + + /** + * Convert to long-form and to + * + */ + $this->data = preg_replace('/\<\?\=/', "data); + $this->data = preg_replace('/<\?(?!xml|php)/s', 'data); + + /** + * file_get_contents() won't update PHP's stat cache, so we grab a stat + * of the file to prevent additional reads should the script be + * requested again, which will make include() happy. + */ + $this->stat = stat($path); + + return true; + } + + /** + * Included so that __FILE__ returns the appropriate info + * + * @return array + */ + public function url_stat() + { + return $this->stat; + } + + /** + * Reads from the stream. + * + * @param int $count + * @return string + */ + public function stream_read($count) + { + $ret = substr($this->data, $this->pos, $count); + $this->pos += strlen($ret); + return $ret; + } + + /** + * Tells the current position in the stream. + * + * @return int + */ + public function stream_tell() + { + return $this->pos; + } + + /** + * Tells if we are at the end of the stream. + * + * @return bool + */ + public function stream_eof() + { + return $this->pos >= strlen($this->data); + } + + /** + * Stream statistics. + * + * @return array + */ + public function stream_stat() + { + return $this->stat; + } + + /** + * Seek to a specific point in the stream. + * + * @param $offset + * @param $whence + * @return bool + */ + public function stream_seek($offset, $whence) + { + switch ($whence) { + case SEEK_SET: + if ($offset < strlen($this->data) && $offset >= 0) { + $this->pos = $offset; + return true; + } else { + return false; + } + break; + + case SEEK_CUR: + if ($offset >= 0) { + $this->pos += $offset; + return true; + } else { + return false; + } + break; + + case SEEK_END: + if (strlen($this->data) + $offset >= 0) { + $this->pos = strlen($this->data) + $offset; + return true; + } else { + return false; + } + break; + + default: + return false; + } + } +} diff --git a/library/Zend/View/Variables.php b/library/Zend/View/Variables.php new file mode 100755 index 0000000000..17b14724c8 --- /dev/null +++ b/library/Zend/View/Variables.php @@ -0,0 +1,162 @@ +setOptions($options); + } + + /** + * Configure object + * + * @param array $options + * @return Variables + */ + public function setOptions(array $options) + { + foreach ($options as $key => $value) { + switch (strtolower($key)) { + case 'strict_vars': + $this->setStrictVars($value); + break; + default: + // Unknown options are considered variables + $this[$key] = $value; + break; + } + } + return $this; + } + + /** + * Set status of "strict vars" flag + * + * @param bool $flag + * @return Variables + */ + public function setStrictVars($flag) + { + $this->strictVars = (bool) $flag; + return $this; + } + + /** + * Are we operating with strict variables? + * + * @return bool + */ + public function isStrict() + { + return $this->strictVars; + } + + /** + * Assign many values at once + * + * @param array|object $spec + * @return Variables + * @throws Exception\InvalidArgumentException + */ + public function assign($spec) + { + if (is_object($spec)) { + if (method_exists($spec, 'toArray')) { + $spec = $spec->toArray(); + } else { + $spec = (array) $spec; + } + } + if (!is_array($spec)) { + throw new Exception\InvalidArgumentException(sprintf( + 'assign() expects either an array or an object as an argument; received "%s"', + gettype($spec) + )); + } + foreach ($spec as $key => $value) { + $this[$key] = $value; + } + + return $this; + } + + /** + * Get the variable value + * + * If the value has not been defined, a null value will be returned; if + * strict vars on in place, a notice will also be raised. + * + * Otherwise, returns _escaped_ version of the value. + * + * @param mixed $key + * @return mixed + */ + public function offsetGet($key) + { + if (!$this->offsetExists($key)) { + if ($this->isStrict()) { + trigger_error(sprintf( + 'View variable "%s" does not exist', $key + ), E_USER_NOTICE); + } + return null; + } + + $return = parent::offsetGet($key); + + // If we have a closure/functor, invoke it, and return its return value + if (is_object($return) && is_callable($return)) { + $return = call_user_func($return); + } + + return $return; + } + + /** + * Clear all variables + * + * @return void + */ + public function clear() + { + $this->exchangeArray(array()); + } +} diff --git a/library/Zend/View/View.php b/library/Zend/View/View.php new file mode 100755 index 0000000000..aebae58a09 --- /dev/null +++ b/library/Zend/View/View.php @@ -0,0 +1,264 @@ +request = $request; + return $this; + } + + /** + * Set MVC response object + * + * @param Response $response + * @return View + */ + public function setResponse(Response $response) + { + $this->response = $response; + return $this; + } + + /** + * Get MVC request object + * + * @return null|Request + */ + public function getRequest() + { + return $this->request; + } + + /** + * Get MVC response object + * + * @return null|Response + */ + public function getResponse() + { + return $this->response; + } + + /** + * Set the event manager instance + * + * @param EventManagerInterface $events + * @return View + */ + public function setEventManager(EventManagerInterface $events) + { + $events->setIdentifiers(array( + __CLASS__, + get_class($this), + )); + $this->events = $events; + return $this; + } + + /** + * Retrieve the event manager instance + * + * Lazy-loads a default instance if none available + * + * @return EventManagerInterface + */ + public function getEventManager() + { + if (!$this->events instanceof EventManagerInterface) { + $this->setEventManager(new EventManager()); + } + return $this->events; + } + + /** + * Add a rendering strategy + * + * Expects a callable. Strategies should accept a ViewEvent object, and should + * return a Renderer instance if the strategy is selected. + * + * Internally, the callable provided will be subscribed to the "renderer" + * event, at the priority specified. + * + * @param callable $callable + * @param int $priority + * @return View + */ + public function addRenderingStrategy($callable, $priority = 1) + { + $this->getEventManager()->attach(ViewEvent::EVENT_RENDERER, $callable, $priority); + return $this; + } + + /** + * Add a response strategy + * + * Expects a callable. Strategies should accept a ViewEvent object. The return + * value will be ignored. + * + * Typical usages for a response strategy are to populate the Response object. + * + * Internally, the callable provided will be subscribed to the "response" + * event, at the priority specified. + * + * @param callable $callable + * @param int $priority + * @return View + */ + public function addResponseStrategy($callable, $priority = 1) + { + $this->getEventManager()->attach(ViewEvent::EVENT_RESPONSE, $callable, $priority); + return $this; + } + + /** + * Render the provided model. + * + * Internally, the following workflow is used: + * + * - Trigger the "renderer" event to select a renderer. + * - Call the selected renderer with the provided Model + * - Trigger the "response" event + * + * @triggers renderer(ViewEvent) + * @triggers response(ViewEvent) + * @param Model $model + * @throws Exception\RuntimeException + * @return void + */ + public function render(Model $model) + { + $event = $this->getEvent(); + $event->setModel($model); + $events = $this->getEventManager(); + $results = $events->trigger(ViewEvent::EVENT_RENDERER, $event, function ($result) { + return ($result instanceof Renderer); + }); + $renderer = $results->last(); + if (!$renderer instanceof Renderer) { + throw new Exception\RuntimeException(sprintf( + '%s: no renderer selected!', + __METHOD__ + )); + } + + $event->setRenderer($renderer); + $events->trigger(ViewEvent::EVENT_RENDERER_POST, $event); + + // If EVENT_RENDERER or EVENT_RENDERER_POST changed the model, make sure + // we use this new model instead of the current $model + $model = $event->getModel(); + + // If we have children, render them first, but only if: + // a) the renderer does not implement TreeRendererInterface, or + // b) it does, but canRenderTrees() returns false + if ($model->hasChildren() + && (!$renderer instanceof TreeRendererInterface + || !$renderer->canRenderTrees()) + ) { + $this->renderChildren($model); + } + + // Reset the model, in case it has changed, and set the renderer + $event->setModel($model); + $event->setRenderer($renderer); + + $rendered = $renderer->render($model); + + // If this is a child model, return the rendered content; do not + // invoke the response strategy. + $options = $model->getOptions(); + if (array_key_exists('has_parent', $options) && $options['has_parent']) { + return $rendered; + } + + $event->setResult($rendered); + + $events->trigger(ViewEvent::EVENT_RESPONSE, $event); + } + + /** + * Loop through children, rendering each + * + * @param Model $model + * @throws Exception\DomainException + * @return void + */ + protected function renderChildren(Model $model) + { + foreach ($model as $child) { + if ($child->terminate()) { + throw new Exception\DomainException('Inconsistent state; child view model is marked as terminal'); + } + $child->setOption('has_parent', true); + $result = $this->render($child); + $child->setOption('has_parent', null); + $capture = $child->captureTo(); + if (!empty($capture)) { + if ($child->isAppend()) { + $oldResult=$model->{$capture}; + $model->setVariable($capture, $oldResult . $result); + } else { + $model->setVariable($capture, $result); + } + } + } + } + + /** + * Create and return ViewEvent used by render() + * + * @return ViewEvent + */ + protected function getEvent() + { + $event = new ViewEvent(); + $event->setTarget($this); + if (null !== ($request = $this->getRequest())) { + $event->setRequest($request); + } + if (null !== ($response = $this->getResponse())) { + $event->setResponse($response); + } + return $event; + } +} diff --git a/library/Zend/View/ViewEvent.php b/library/Zend/View/ViewEvent.php new file mode 100755 index 0000000000..34baa2136f --- /dev/null +++ b/library/Zend/View/ViewEvent.php @@ -0,0 +1,258 @@ +model = $model; + return $this; + } + + /** + * Set the MVC request object + * + * @param Request $request + * @return ViewEvent + */ + public function setRequest(Request $request) + { + $this->request = $request; + return $this; + } + + /** + * Set the MVC response object + * + * @param Response $response + * @return ViewEvent + */ + public function setResponse(Response $response) + { + $this->response = $response; + return $this; + } + + /** + * Set result of rendering + * + * @param mixed $result + * @return ViewEvent + */ + public function setResult($result) + { + $this->result = $result; + return $this; + } + + /** + * Retrieve the view model + * + * @return null|Model + */ + public function getModel() + { + return $this->model; + } + + /** + * Set value for renderer + * + * @param Renderer $renderer + * @return ViewEvent + */ + public function setRenderer(Renderer $renderer) + { + $this->renderer = $renderer; + return $this; + } + + /** + * Get value for renderer + * + * @return null|Renderer + */ + public function getRenderer() + { + return $this->renderer; + } + + /** + * Retrieve the MVC request object + * + * @return null|Request + */ + public function getRequest() + { + return $this->request; + } + + /** + * Retrieve the MVC response object + * + * @return null|Response + */ + public function getResponse() + { + return $this->response; + } + + /** + * Retrieve the result of rendering + * + * @return mixed + */ + public function getResult() + { + return $this->result; + } + + /** + * Get event parameter + * + * @param string $name + * @param mixed $default + * @return mixed + */ + public function getParam($name, $default = null) + { + switch ($name) { + case 'model': + return $this->getModel(); + case 'renderer': + return $this->getRenderer(); + case 'request': + return $this->getRequest(); + case 'response': + return $this->getResponse(); + case 'result': + return $this->getResult(); + default: + return parent::getParam($name, $default); + } + } + + /** + * Get all event parameters + * + * @return array|\ArrayAccess + */ + public function getParams() + { + $params = parent::getParams(); + $params['model'] = $this->getModel(); + $params['renderer'] = $this->getRenderer(); + $params['request'] = $this->getRequest(); + $params['response'] = $this->getResponse(); + $params['result'] = $this->getResult(); + return $params; + } + + /** + * Set event parameters + * + * @param array|object|ArrayAccess $params + * @return ViewEvent + */ + public function setParams($params) + { + parent::setParams($params); + if (!is_array($params) && !$params instanceof ArrayAccess) { + return $this; + } + + foreach (array('model', 'renderer', 'request', 'response', 'result') as $param) { + if (isset($params[$param])) { + $method = 'set' . $param; + $this->$method($params[$param]); + } + } + return $this; + } + + /** + * Set an individual event parameter + * + * @param string $name + * @param mixed $value + * @return ViewEvent + */ + public function setParam($name, $value) + { + switch ($name) { + case 'model': + $this->setModel($value); + break; + case 'renderer': + $this->setRenderer($value); + break; + case 'request': + $this->setRequest($value); + break; + case 'response': + $this->setResponse($value); + break; + case 'result': + $this->setResult($value); + break; + default: + parent::setParam($name, $value); + break; + } + return $this; + } +} diff --git a/library/Zend/View/composer.json b/library/Zend/View/composer.json new file mode 100755 index 0000000000..44bb5c2751 --- /dev/null +++ b/library/Zend/View/composer.json @@ -0,0 +1,58 @@ +{ + "name": "zendframework/zend-view", + "description": "provides a system of helpers, output filters, and variable escaping", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "view" + ], + "homepage": "https://github.com/zendframework/zf2", + "autoload": { + "psr-0": { + "Zend\\View\\": "" + } + }, + "target-dir": "Zend/View", + "require": { + "php": ">=5.3.23", + "zendframework/zend-eventmanager": "self.version", + "zendframework/zend-loader": "self.version", + "zendframework/zend-stdlib": "self.version" + }, + "require-dev": { + "zendframework/zend-authentication": "self.version", + "zendframework/zend-escaper": "self.version", + "zendframework/zend-feed": "self.version", + "zendframework/zend-filter": "self.version", + "zendframework/zend-http": "self.version", + "zendframework/zend-i18n": "self.version", + "zendframework/zend-json": "self.version", + "zendframework/zend-mvc": "self.version", + "zendframework/zend-navigation": "self.version", + "zendframework/zend-paginator": "self.version", + "zendframework/zend-permissions-acl": "self.version", + "zendframework/zend-servicemanager": "self.version", + "zendframework/zend-uri": "self.version" + }, + "suggest": { + "zendframework/zend-authentication": "Zend\\Authentication component", + "zendframework/zend-escaper": "Zend\\Escaper component", + "zendframework/zend-feed": "Zend\\Feed component", + "zendframework/zend-filter": "Zend\\Filter component", + "zendframework/zend-http": "Zend\\Http component", + "zendframework/zend-i18n": "Zend\\I18n component", + "zendframework/zend-json": "Zend\\Json component", + "zendframework/zend-mvc": "Zend\\Mvc component", + "zendframework/zend-navigation": "Zend\\Navigation component", + "zendframework/zend-paginator": "Zend\\Paginator component", + "zendframework/zend-permissions-acl": "Zend\\Permissions\\Acl component", + "zendframework/zend-servicemanager": "Zend\\ServiceManager component", + "zendframework/zend-uri": "Zend\\Uri component" + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev", + "dev-develop": "2.4-dev" + } + } +} diff --git a/library/Zend/XmlRpc/AbstractValue.php b/library/Zend/XmlRpc/AbstractValue.php new file mode 100755 index 0000000000..221238a094 --- /dev/null +++ b/library/Zend/XmlRpc/AbstractValue.php @@ -0,0 +1,462 @@ +type; + } + + /** + * Get XML generator instance + * + * @return \Zend\XmlRpc\Generator\GeneratorInterface + */ + public static function getGenerator() + { + if (!static::$generator) { + if (extension_loaded('xmlwriter')) { + static::$generator = new Generator\XmlWriter(); + } else { + static::$generator = new Generator\DomDocument(); + } + } + + return static::$generator; + } + + /** + * Sets XML generator instance + * + * @param null|Generator\GeneratorInterface $generator + * @return void + */ + public static function setGenerator(Generator\GeneratorInterface $generator = null) + { + static::$generator = $generator; + } + + /** + * Changes the encoding of the generator + * + * @param string $encoding + * @return void + */ + public static function setEncoding($encoding) + { + $generator = static::getGenerator(); + $newGenerator = new $generator($encoding); + static::setGenerator($newGenerator); + } + + /** + * Return the value of this object, convert the XML-RPC native value into a PHP variable + * + * @return mixed + */ + abstract public function getValue(); + + + /** + * Return the XML code that represent a native MXL-RPC value + * + * @return string + */ + public function saveXml() + { + if (!$this->xml) { + $this->generateXml(); + $this->xml = (string) $this->getGenerator(); + } + return $this->xml; + } + + /** + * Generate XML code that represent a native XML/RPC value + * + * @return void + */ + public function generateXml() + { + $this->_generateXml(); + } + + /** + * Creates a Value* object, representing a native XML-RPC value + * A XmlRpcValue object can be created in 3 ways: + * 1. Autodetecting the native type out of a PHP variable + * (if $type is not set or equal to Value::AUTO_DETECT_TYPE) + * 2. By specifying the native type ($type is one of the Value::XMLRPC_TYPE_* constants) + * 3. From a XML string ($type is set to Value::XML_STRING) + * + * By default the value type is autodetected according to it's PHP type + * + * @param mixed $value + * @param Zend\XmlRpc\Value::constant $type + * @throws Exception\ValueException + * @return AbstractValue + */ + public static function getXmlRpcValue($value, $type = self::AUTO_DETECT_TYPE) + { + switch ($type) { + case self::AUTO_DETECT_TYPE: + // Auto detect the XML-RPC native type from the PHP type of $value + return static::_phpVarToNativeXmlRpc($value); + + case self::XML_STRING: + // Parse the XML string given in $value and get the XML-RPC value in it + return static::_xmlStringToNativeXmlRpc($value); + + case self::XMLRPC_TYPE_I4: + // fall through to the next case + case self::XMLRPC_TYPE_INTEGER: + return new Value\Integer($value); + + case self::XMLRPC_TYPE_I8: + // fall through to the next case + case self::XMLRPC_TYPE_APACHEI8: + return new Value\BigInteger($value); + + case self::XMLRPC_TYPE_DOUBLE: + return new Value\Double($value); + + case self::XMLRPC_TYPE_BOOLEAN: + return new Value\Boolean($value); + + case self::XMLRPC_TYPE_STRING: + return new Value\String($value); + + case self::XMLRPC_TYPE_BASE64: + return new Value\Base64($value); + + case self::XMLRPC_TYPE_NIL: + // fall through to the next case + case self::XMLRPC_TYPE_APACHENIL: + return new Value\Nil(); + + case self::XMLRPC_TYPE_DATETIME: + return new Value\DateTime($value); + + case self::XMLRPC_TYPE_ARRAY: + return new Value\ArrayValue($value); + + case self::XMLRPC_TYPE_STRUCT: + return new Value\Struct($value); + + default: + throw new Exception\ValueException('Given type is not a '. __CLASS__ .' constant'); + } + } + + /** + * Get XML-RPC type for a PHP native variable + * + * @static + * @param mixed $value + * @throws Exception\InvalidArgumentException + * @return string + */ + public static function getXmlRpcTypeByValue($value) + { + if (is_object($value)) { + if ($value instanceof AbstractValue) { + return $value->getType(); + } elseif ($value instanceof DateTime) { + return self::XMLRPC_TYPE_DATETIME; + } + return static::getXmlRpcTypeByValue(get_object_vars($value)); + } elseif (is_array($value)) { + if (!empty($value) && is_array($value) && (array_keys($value) !== range(0, count($value) - 1))) { + return self::XMLRPC_TYPE_STRUCT; + } + return self::XMLRPC_TYPE_ARRAY; + } elseif (is_int($value)) { + return ($value > PHP_INT_MAX) ? self::XMLRPC_TYPE_I8 : self::XMLRPC_TYPE_INTEGER; + } elseif (is_double($value)) { + return self::XMLRPC_TYPE_DOUBLE; + } elseif (is_bool($value)) { + return self::XMLRPC_TYPE_BOOLEAN; + } elseif (null === $value) { + return self::XMLRPC_TYPE_NIL; + } elseif (is_string($value)) { + return self::XMLRPC_TYPE_STRING; + } + throw new Exception\InvalidArgumentException(sprintf( + 'No matching XMLRPC type found for php type %s.', + gettype($value) + )); + } + + /** + * Transform a PHP native variable into a XML-RPC native value + * + * @param mixed $value The PHP variable for conversion + * + * @throws Exception\InvalidArgumentException + * @return AbstractValue + * @static + */ + protected static function _phpVarToNativeXmlRpc($value) + { + // @see http://framework.zend.com/issues/browse/ZF-8623 + if ($value instanceof AbstractValue) { + return $value; + } + + switch (static::getXmlRpcTypeByValue($value)) { + case self::XMLRPC_TYPE_DATETIME: + return new Value\DateTime($value); + + case self::XMLRPC_TYPE_ARRAY: + return new Value\ArrayValue($value); + + case self::XMLRPC_TYPE_STRUCT: + return new Value\Struct($value); + + case self::XMLRPC_TYPE_INTEGER: + return new Value\Integer($value); + + case self::XMLRPC_TYPE_DOUBLE: + return new Value\Double($value); + + case self::XMLRPC_TYPE_BOOLEAN: + return new Value\Boolean($value); + + case self::XMLRPC_TYPE_NIL: + return new Value\Nil; + + case self::XMLRPC_TYPE_STRING: + // Fall through to the next case + default: + // If type isn't identified (or identified as string), it treated as string + return new Value\String($value); + } + } + + /** + * Transform an XML string into a XML-RPC native value + * + * @param string|\SimpleXMLElement $xml A SimpleXMLElement object represent the XML string + * It can be also a valid XML string for conversion + * + * @throws Exception\ValueException + * @return \Zend\XmlRpc\AbstractValue + * @static + */ + protected static function _xmlStringToNativeXmlRpc($xml) + { + static::_createSimpleXMLElement($xml); + + static::_extractTypeAndValue($xml, $type, $value); + + switch ($type) { + // All valid and known XML-RPC native values + case self::XMLRPC_TYPE_I4: + // Fall through to the next case + case self::XMLRPC_TYPE_INTEGER: + $xmlrpcValue = new Value\Integer($value); + break; + case self::XMLRPC_TYPE_APACHEI8: + // Fall through to the next case + case self::XMLRPC_TYPE_I8: + $xmlrpcValue = new Value\BigInteger($value); + break; + case self::XMLRPC_TYPE_DOUBLE: + $xmlrpcValue = new Value\Double($value); + break; + case self::XMLRPC_TYPE_BOOLEAN: + $xmlrpcValue = new Value\Boolean($value); + break; + case self::XMLRPC_TYPE_STRING: + $xmlrpcValue = new Value\String($value); + break; + case self::XMLRPC_TYPE_DATETIME: // The value should already be in an iso8601 format + $xmlrpcValue = new Value\DateTime($value); + break; + case self::XMLRPC_TYPE_BASE64: // The value should already be base64 encoded + $xmlrpcValue = new Value\Base64($value, true); + break; + case self::XMLRPC_TYPE_NIL: + // Fall through to the next case + case self::XMLRPC_TYPE_APACHENIL: + // The value should always be NULL + $xmlrpcValue = new Value\Nil(); + break; + case self::XMLRPC_TYPE_ARRAY: + // PHP 5.2.4 introduced a regression in how empty($xml->value) + // returns; need to look for the item specifically + $data = null; + foreach ($value->children() as $key => $value) { + if ('data' == $key) { + $data = $value; + break; + } + } + + if (null === $data) { + throw new Exception\ValueException('Invalid XML for XML-RPC native '. self::XMLRPC_TYPE_ARRAY .' type: ARRAY tag must contain DATA tag'); + } + $values = array(); + // Parse all the elements of the array from the XML string + // (simple xml element) to Value objects + foreach ($data->value as $element) { + $values[] = static::_xmlStringToNativeXmlRpc($element); + } + $xmlrpcValue = new Value\ArrayValue($values); + break; + case self::XMLRPC_TYPE_STRUCT: + $values = array(); + // Parse all the members of the struct from the XML string + // (simple xml element) to Value objects + foreach ($value->member as $member) { + // @todo? If a member doesn't have a tag, we don't add it to the struct + // Maybe we want to throw an exception here ? + if (!isset($member->value) or !isset($member->name)) { + continue; + //throw new Value_Exception('Member of the '. self::XMLRPC_TYPE_STRUCT .' XML-RPC native type must contain a VALUE tag'); + } + $values[(string) $member->name] = static::_xmlStringToNativeXmlRpc($member->value); + } + $xmlrpcValue = new Value\Struct($values); + break; + default: + throw new Exception\ValueException('Value type \''. $type .'\' parsed from the XML string is not a known XML-RPC native type'); + break; + } + $xmlrpcValue->_setXML($xml->asXML()); + + return $xmlrpcValue; + } + + protected static function _createSimpleXMLElement(&$xml) + { + if ($xml instanceof \SimpleXMLElement) { + return; + } + + try { + $xml = new \SimpleXMLElement($xml); + } catch (\Exception $e) { + // The given string is not a valid XML + throw new Exception\ValueException('Failed to create XML-RPC value from XML string: ' . $e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Extract XML/RPC type and value from SimpleXMLElement object + * + * @param \SimpleXMLElement $xml + * @param string &$type Type bind variable + * @param string &$value Value bind variable + * @return void + */ + protected static function _extractTypeAndValue(\SimpleXMLElement $xml, &$type, &$value) + { + list($type, $value) = each($xml); + if (!$type and $value === null) { + $namespaces = array('ex' => 'http://ws.apache.org/xmlrpc/namespaces/extensions'); + foreach ($namespaces as $namespaceName => $namespaceUri) { + $namespaceXml = $xml->children($namespaceUri); + list($type, $value) = each($namespaceXml); + if ($type !== null) { + $type = $namespaceName . ':' . $type; + break; + } + } + } + + // If no type was specified, the default is string + if (!$type) { + $type = self::XMLRPC_TYPE_STRING; + if (empty($value) and preg_match('#^.*$#', $xml->asXML())) { + $value = str_replace(array('', ''), '', $xml->asXML()); + } + } + } + + /** + * @param $xml + * @return void + */ + protected function _setXML($xml) + { + $this->xml = $this->getGenerator()->stripDeclaration($xml); + } +} diff --git a/library/Zend/XmlRpc/CONTRIBUTING.md b/library/Zend/XmlRpc/CONTRIBUTING.md new file mode 100755 index 0000000000..e77f5d2d5b --- /dev/null +++ b/library/Zend/XmlRpc/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# CONTRIBUTING + +Please don't open pull requests against this repository, please use https://github.com/zendframework/zf2. \ No newline at end of file diff --git a/library/Zend/XmlRpc/Client.php b/library/Zend/XmlRpc/Client.php new file mode 100755 index 0000000000..504a3bf231 --- /dev/null +++ b/library/Zend/XmlRpc/Client.php @@ -0,0 +1,343 @@ +httpClient = new Http\Client(); + } else { + $this->httpClient = $httpClient; + } + + $this->introspector = new Client\ServerIntrospection($this); + $this->serverAddress = $server; + } + + + /** + * Sets the HTTP client object to use for connecting the XML-RPC server. + * + * @param \Zend\Http\Client $httpClient + * @return \Zend\Http\Client + */ + public function setHttpClient(Http\Client $httpClient) + { + return $this->httpClient = $httpClient; + } + + + /** + * Gets the HTTP client object. + * + * @return \Zend\Http\Client + */ + public function getHttpClient() + { + return $this->httpClient; + } + + + /** + * Sets the object used to introspect remote servers + * + * @param \Zend\XmlRpc\Client\ServerIntrospection + * @return \Zend\XmlRpc\Client\ServerIntrospection + */ + public function setIntrospector(Client\ServerIntrospection $introspector) + { + return $this->introspector = $introspector; + } + + + /** + * Gets the introspection object. + * + * @return \Zend\XmlRpc\Client\ServerIntrospection + */ + public function getIntrospector() + { + return $this->introspector; + } + + + /** + * The request of the last method call + * + * @return \Zend\XmlRpc\Request + */ + public function getLastRequest() + { + return $this->lastRequest; + } + + + /** + * The response received from the last method call + * + * @return \Zend\XmlRpc\Response + */ + public function getLastResponse() + { + return $this->lastResponse; + } + + + /** + * Returns a proxy object for more convenient method calls + * + * @param string $namespace Namespace to proxy or empty string for none + * @return \Zend\XmlRpc\Client\ServerProxy + */ + public function getProxy($namespace = '') + { + if (empty($this->proxyCache[$namespace])) { + $proxy = new Client\ServerProxy($this, $namespace); + $this->proxyCache[$namespace] = $proxy; + } + return $this->proxyCache[$namespace]; + } + + /** + * Set skip system lookup flag + * + * @param bool $flag + * @return \Zend\XmlRpc\Client + */ + public function setSkipSystemLookup($flag = true) + { + $this->skipSystemLookup = (bool) $flag; + return $this; + } + + /** + * Skip system lookup when determining if parameter should be array or struct? + * + * @return bool + */ + public function skipSystemLookup() + { + return $this->skipSystemLookup; + } + + /** + * Perform an XML-RPC request and return a response. + * + * @param \Zend\XmlRpc\Request $request + * @param null|\Zend\XmlRpc\Response $response + * @return void + * @throws \Zend\XmlRpc\Client\Exception\HttpException + */ + public function doRequest($request, $response = null) + { + $this->lastRequest = $request; + + if (PHP_VERSION_ID < 50600) { + iconv_set_encoding('input_encoding', 'UTF-8'); + iconv_set_encoding('output_encoding', 'UTF-8'); + iconv_set_encoding('internal_encoding', 'UTF-8'); + } else { + ini_set('default_charset', 'UTF-8'); + } + + $http = $this->getHttpClient(); + $httpRequest = $http->getRequest(); + if ($httpRequest->getUriString() === null) { + $http->setUri($this->serverAddress); + } + + $headers = $httpRequest->getHeaders(); + $headers->addHeaders(array( + 'Content-Type: text/xml; charset=utf-8', + 'Accept: text/xml', + )); + + if (!$headers->get('user-agent')) { + $headers->addHeaderLine('user-agent', 'Zend_XmlRpc_Client'); + } + + $xml = $this->lastRequest->__toString(); + $http->setRawBody($xml); + $httpResponse = $http->setMethod('POST')->send(); + + if (!$httpResponse->isSuccess()) { + /** + * Exception thrown when an HTTP error occurs + */ + throw new Client\Exception\HttpException( + $httpResponse->getReasonPhrase(), + $httpResponse->getStatusCode() + ); + } + + if ($response === null) { + $response = new Response(); + } + + $this->lastResponse = $response; + $this->lastResponse->loadXml(trim($httpResponse->getBody())); + } + + /** + * Send an XML-RPC request to the service (for a specific method) + * + * @param string $method Name of the method we want to call + * @param array $params Array of parameters for the method + * @return mixed + * @throws \Zend\XmlRpc\Client\Exception\FaultException + */ + public function call($method, $params=array()) + { + if (!$this->skipSystemLookup() && ('system.' != substr($method, 0, 7))) { + // Ensure empty array/struct params are cast correctly + // If system.* methods are not available, bypass. (ZF-2978) + $success = true; + try { + $signatures = $this->getIntrospector()->getMethodSignature($method); + } catch (\Zend\XmlRpc\Exception\ExceptionInterface $e) { + $success = false; + } + if ($success) { + $validTypes = array( + AbstractValue::XMLRPC_TYPE_ARRAY, + AbstractValue::XMLRPC_TYPE_BASE64, + AbstractValue::XMLRPC_TYPE_BOOLEAN, + AbstractValue::XMLRPC_TYPE_DATETIME, + AbstractValue::XMLRPC_TYPE_DOUBLE, + AbstractValue::XMLRPC_TYPE_I4, + AbstractValue::XMLRPC_TYPE_INTEGER, + AbstractValue::XMLRPC_TYPE_NIL, + AbstractValue::XMLRPC_TYPE_STRING, + AbstractValue::XMLRPC_TYPE_STRUCT, + ); + + if (!is_array($params)) { + $params = array($params); + } + foreach ($params as $key => $param) { + if ($param instanceof AbstractValue) { + continue; + } + + if (count($signatures) > 1) { + $type = AbstractValue::getXmlRpcTypeByValue($param); + foreach ($signatures as $signature) { + if (!is_array($signature)) { + continue; + } + if (isset($signature['parameters'][$key])) { + if ($signature['parameters'][$key] == $type) { + break; + } + } + } + } elseif (isset($signatures[0]['parameters'][$key])) { + $type = $signatures[0]['parameters'][$key]; + } else { + $type = null; + } + + if (empty($type) || !in_array($type, $validTypes)) { + $type = AbstractValue::AUTO_DETECT_TYPE; + } + + $params[$key] = AbstractValue::getXmlRpcValue($param, $type); + } + } + } + + $request = $this->_createRequest($method, $params); + + $this->doRequest($request); + + if ($this->lastResponse->isFault()) { + $fault = $this->lastResponse->getFault(); + /** + * Exception thrown when an XML-RPC fault is returned + */ + throw new Client\Exception\FaultException( + $fault->getMessage(), + $fault->getCode() + ); + } + + return $this->lastResponse->getReturnValue(); + } + + /** + * Create request object + * + * @param string $method + * @param array $params + * @return \Zend\XmlRpc\Request + */ + protected function _createRequest($method, $params) + { + return new Request($method, $params); + } +} diff --git a/library/Zend/XmlRpc/Client/Exception/ExceptionInterface.php b/library/Zend/XmlRpc/Client/Exception/ExceptionInterface.php new file mode 100755 index 0000000000..03c09959c7 --- /dev/null +++ b/library/Zend/XmlRpc/Client/Exception/ExceptionInterface.php @@ -0,0 +1,19 @@ +system = $client->getProxy('system'); + } + + /** + * Returns the signature for each method on the server, + * autodetecting whether system.multicall() is supported and + * using it if so. + * + * @return array + */ + public function getSignatureForEachMethod() + { + $methods = $this->listMethods(); + + try { + $signatures = $this->getSignatureForEachMethodByMulticall($methods); + } catch (Exception\FaultException $e) { + // degrade to looping + } + + if (empty($signatures)) { + $signatures = $this->getSignatureForEachMethodByLooping($methods); + } + + return $signatures; + } + + /** + * Attempt to get the method signatures in one request via system.multicall(). + * This is a boxcar feature of XML-RPC and is found on fewer servers. However, + * can significantly improve performance if present. + * + * @param array $methods + * @throws Exception\IntrospectException + * @return array array(array(return, param, param, param...)) + */ + public function getSignatureForEachMethodByMulticall($methods = null) + { + if ($methods === null) { + $methods = $this->listMethods(); + } + + $multicallParams = array(); + foreach ($methods as $method) { + $multicallParams[] = array('methodName' => 'system.methodSignature', + 'params' => array($method)); + } + + $serverSignatures = $this->system->multicall($multicallParams); + + if (! is_array($serverSignatures)) { + $type = gettype($serverSignatures); + $error = "Multicall return is malformed. Expected array, got $type"; + throw new Exception\IntrospectException($error); + } + + if (count($serverSignatures) != count($methods)) { + $error = 'Bad number of signatures received from multicall'; + throw new Exception\IntrospectException($error); + } + + // Create a new signatures array with the methods name as keys and the signature as value + $signatures = array(); + foreach ($serverSignatures as $i => $signature) { + $signatures[$methods[$i]] = $signature; + } + + return $signatures; + } + + /** + * Get the method signatures for every method by + * successively calling system.methodSignature + * + * @param array $methods + * @return array + */ + public function getSignatureForEachMethodByLooping($methods = null) + { + if ($methods === null) { + $methods = $this->listMethods(); + } + + $signatures = array(); + foreach ($methods as $method) { + $signatures[$method] = $this->getMethodSignature($method); + } + + return $signatures; + } + + /** + * Call system.methodSignature() for the given method + * + * @param array $method + * @throws Exception\IntrospectException + * @return array array(array(return, param, param, param...)) + */ + public function getMethodSignature($method) + { + $signature = $this->system->methodSignature($method); + if (!is_array($signature)) { + $error = 'Invalid signature for method "' . $method . '"'; + throw new Exception\IntrospectException($error); + } + return $signature; + } + + /** + * Call system.listMethods() + * + * @return array array(method, method, method...) + */ + public function listMethods() + { + return $this->system->listMethods(); + } +} diff --git a/library/Zend/XmlRpc/Client/ServerProxy.php b/library/Zend/XmlRpc/Client/ServerProxy.php new file mode 100755 index 0000000000..861b615905 --- /dev/null +++ b/library/Zend/XmlRpc/Client/ServerProxy.php @@ -0,0 +1,79 @@ +foo->bar->baz()". + */ +class ServerProxy +{ + /** + * @var \Zend\XmlRpc\Client + */ + private $client = null; + + /** + * @var string + */ + private $namespace = ''; + + + /** + * @var array of \Zend\XmlRpc\Client\ServerProxy + */ + private $cache = array(); + + + /** + * Class constructor + * + * @param \Zend\XmlRpc\Client $client + * @param string $namespace + */ + public function __construct(XMLRPCClient $client, $namespace = '') + { + $this->client = $client; + $this->namespace = $namespace; + } + + + /** + * Get the next successive namespace + * + * @param string $namespace + * @return \Zend\XmlRpc\Client\ServerProxy + */ + public function __get($namespace) + { + $namespace = ltrim("$this->namespace.$namespace", '.'); + if (!isset($this->cache[$namespace])) { + $this->cache[$namespace] = new $this($this->client, $namespace); + } + return $this->cache[$namespace]; + } + + + /** + * Call a method in this namespace. + * + * @param string $method + * @param array $args + * @return mixed + */ + public function __call($method, $args) + { + $method = ltrim("{$this->namespace}.{$method}", '.'); + return $this->client->call($method, $args); + } +} diff --git a/library/Zend/XmlRpc/Exception/BadMethodCallException.php b/library/Zend/XmlRpc/Exception/BadMethodCallException.php new file mode 100755 index 0000000000..db36424ab9 --- /dev/null +++ b/library/Zend/XmlRpc/Exception/BadMethodCallException.php @@ -0,0 +1,14 @@ + messages + * @var array + */ + protected $internal = array( + 404 => 'Unknown Error', + + // 610 - 619 reflection errors + 610 => 'Invalid method class', + 611 => 'Unable to attach function or callback; not callable', + 612 => 'Unable to load array; not an array', + 613 => 'One or more method records are corrupt or otherwise unusable', + + // 620 - 629 dispatch errors + 620 => 'Method does not exist', + 621 => 'Error instantiating class to invoke method', + 622 => 'Method missing implementation', + 623 => 'Calling parameters do not match signature', + + // 630 - 639 request errors + 630 => 'Unable to read request', + 631 => 'Failed to parse request', + 632 => 'Invalid request, no method passed; request must contain a \'methodName\' tag', + 633 => 'Param must contain a value', + 634 => 'Invalid method name', + 635 => 'Invalid XML provided to request', + 636 => 'Error creating xmlrpc value', + + // 640 - 649 system.* errors + 640 => 'Method does not exist', + + // 650 - 659 response errors + 650 => 'Invalid XML provided for response', + 651 => 'Failed to parse response', + 652 => 'Invalid response', + 653 => 'Invalid XMLRPC value in response', + ); + + /** + * Constructor + * + */ + public function __construct($code = 404, $message = '') + { + $this->setCode($code); + $code = $this->getCode(); + + if (empty($message) && isset($this->internal[$code])) { + $message = $this->internal[$code]; + } elseif (empty($message)) { + $message = 'Unknown error'; + } + $this->setMessage($message); + } + + /** + * Set the fault code + * + * @param int $code + * @return Fault + */ + public function setCode($code) + { + $this->code = (int) $code; + return $this; + } + + /** + * Return fault code + * + * @return int + */ + public function getCode() + { + return $this->code; + } + + /** + * Retrieve fault message + * + * @param string + * @return Fault + */ + public function setMessage($message) + { + $this->message = (string) $message; + return $this; + } + + /** + * Retrieve fault message + * + * @return string + */ + public function getMessage() + { + return $this->message; + } + + /** + * Set encoding to use in fault response + * + * @param string $encoding + * @return Fault + */ + public function setEncoding($encoding) + { + $this->encoding = $encoding; + AbstractValue::setEncoding($encoding); + return $this; + } + + /** + * Retrieve current fault encoding + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Load an XMLRPC fault from XML + * + * @param string $fault + * @return bool Returns true if successfully loaded fault response, false + * if response was not a fault response + * @throws Exception\ExceptionInterface if no or faulty XML provided, or if fault + * response does not contain either code or message + */ + public function loadXml($fault) + { + if (!is_string($fault)) { + throw new Exception\InvalidArgumentException('Invalid XML provided to fault'); + } + + $xmlErrorsFlag = libxml_use_internal_errors(true); + try { + $xml = XmlSecurity::scan($fault); + } catch (\ZendXml\Exception\RuntimeException $e) { + // Unsecure XML + throw new Exception\RuntimeException('Failed to parse XML fault: ' . $e->getMessage(), 500, $e); + } + if (!$xml instanceof SimpleXMLElement) { + $errors = libxml_get_errors(); + $errors = array_reduce($errors, function ($result, $item) { + if (empty($result)) { + return $item->message; + } + return $result . '; ' . $item->message; + }, ''); + libxml_use_internal_errors($xmlErrorsFlag); + throw new Exception\InvalidArgumentException('Failed to parse XML fault: ' . $errors, 500); + } + libxml_use_internal_errors($xmlErrorsFlag); + + // Check for fault + if (!$xml->fault) { + // Not a fault + return false; + } + + if (!$xml->fault->value->struct) { + // not a proper fault + throw new Exception\InvalidArgumentException('Invalid fault structure', 500); + } + + $structXml = $xml->fault->value->asXML(); + $struct = AbstractValue::getXmlRpcValue($structXml, AbstractValue::XML_STRING); + $struct = $struct->getValue(); + + if (isset($struct['faultCode'])) { + $code = $struct['faultCode']; + } + if (isset($struct['faultString'])) { + $message = $struct['faultString']; + } + + if (empty($code) && empty($message)) { + throw new Exception\InvalidArgumentException('Fault code and string required'); + } + + if (empty($code)) { + $code = '404'; + } + + if (empty($message)) { + if (isset($this->internal[$code])) { + $message = $this->internal[$code]; + } else { + $message = 'Unknown Error'; + } + } + + $this->setCode($code); + $this->setMessage($message); + + return true; + } + + /** + * Determine if an XML response is an XMLRPC fault + * + * @param string $xml + * @return bool + */ + public static function isFault($xml) + { + $fault = new static(); + try { + $isFault = $fault->loadXml($xml); + } catch (Exception\ExceptionInterface $e) { + $isFault = false; + } + + return $isFault; + } + + /** + * Serialize fault to XML + * + * @return string + */ + public function saveXml() + { + // Create fault value + $faultStruct = array( + 'faultCode' => $this->getCode(), + 'faultString' => $this->getMessage() + ); + $value = AbstractValue::getXmlRpcValue($faultStruct); + + $generator = AbstractValue::getGenerator(); + $generator->openElement('methodResponse') + ->openElement('fault'); + $value->generateXml(); + $generator->closeElement('fault') + ->closeElement('methodResponse'); + + return $generator->flush(); + } + + /** + * Return XML fault response + * + * @return string + */ + public function __toString() + { + return $this->saveXML(); + } +} diff --git a/library/Zend/XmlRpc/Generator/AbstractGenerator.php b/library/Zend/XmlRpc/Generator/AbstractGenerator.php new file mode 100755 index 0000000000..693f026c09 --- /dev/null +++ b/library/Zend/XmlRpc/Generator/AbstractGenerator.php @@ -0,0 +1,151 @@ +setEncoding($encoding); + $this->_init(); + } + + /** + * Initialize internal objects + * + * @return void + */ + abstract protected function _init(); + + /** + * Start XML element + * + * Method opens a new XML element with an element name and an optional value + * + * @param string $name XML tag name + * @param string $value Optional value of the XML tag + * @return AbstractGenerator Fluent interface + */ + public function openElement($name, $value = null) + { + $this->_openElement($name); + if ($value !== null) { + $this->_writeTextData($value); + } + + return $this; + } + + /** + * End of an XML element + * + * Method marks the end of an XML element + * + * @param string $name XML tag name + * @return AbstractGenerator Fluent interface + */ + public function closeElement($name) + { + $this->_closeElement($name); + + return $this; + } + + /** + * Return encoding + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Set XML encoding + * + * @param string $encoding + * @return AbstractGenerator + */ + public function setEncoding($encoding) + { + $this->encoding = $encoding; + return $this; + } + + /** + * Returns the XML as a string and flushes all internal buffers + * + * @return string + */ + public function flush() + { + $xml = $this->saveXml(); + $this->_init(); + return $xml; + } + + /** + * Returns XML without document declaration + * + * @return string + */ + public function __toString() + { + return $this->stripDeclaration($this->saveXml()); + } + + /** + * Removes XML declaration from a string + * + * @param string $xml + * @return string + */ + public function stripDeclaration($xml) + { + return preg_replace('/<\?xml version="1.0"( encoding="[^\"]*")?\?>\n/u', '', $xml); + } + + /** + * Start XML element + * + * @param string $name XML element name + */ + abstract protected function _openElement($name); + + /** + * Write XML text data into the currently opened XML element + * + * @param string $text + */ + abstract protected function _writeTextData($text); + + /** + * End XML element + * + * @param string $name + */ + abstract protected function _closeElement($name); +} diff --git a/library/Zend/XmlRpc/Generator/DomDocument.php b/library/Zend/XmlRpc/Generator/DomDocument.php new file mode 100755 index 0000000000..47e7a72238 --- /dev/null +++ b/library/Zend/XmlRpc/Generator/DomDocument.php @@ -0,0 +1,85 @@ +dom->createElement($name); + + $this->currentElement = $this->currentElement->appendChild($newElement); + } + + /** + * Write XML text data into the currently opened XML element + * + * @param string $text + */ + protected function _writeTextData($text) + { + $this->currentElement->appendChild($this->dom->createTextNode($text)); + } + + /** + * Close a previously opened XML element + * + * Resets $currentElement to the next parent node in the hierarchy + * + * @param string $name + * @return void + */ + protected function _closeElement($name) + { + if (isset($this->currentElement->parentNode)) { + $this->currentElement = $this->currentElement->parentNode; + } + } + + /** + * Save XML as a string + * + * @return string + */ + public function saveXml() + { + return $this->dom->saveXml(); + } + + /** + * Initializes internal objects + * + * @return void + */ + protected function _init() + { + $this->dom = new \DOMDocument('1.0', $this->encoding); + $this->currentElement = $this->dom; + } +} diff --git a/library/Zend/XmlRpc/Generator/GeneratorInterface.php b/library/Zend/XmlRpc/Generator/GeneratorInterface.php new file mode 100755 index 0000000000..ca2af243b2 --- /dev/null +++ b/library/Zend/XmlRpc/Generator/GeneratorInterface.php @@ -0,0 +1,32 @@ +xmlWriter = new \XMLWriter(); + $this->xmlWriter->openMemory(); + $this->xmlWriter->startDocument('1.0', $this->encoding); + } + + + /** + * Open a new XML element + * + * @param string $name XML element name + * @return void + */ + protected function _openElement($name) + { + $this->xmlWriter->startElement($name); + } + + /** + * Write XML text data into the currently opened XML element + * + * @param string $text XML text data + * @return void + */ + protected function _writeTextData($text) + { + $this->xmlWriter->text($text); + } + + /** + * Close a previously opened XML element + * + * @param string $name + * @return XmlWriter + */ + protected function _closeElement($name) + { + $this->xmlWriter->endElement(); + + return $this; + } + + /** + * Emit XML document + * + * @return string + */ + public function saveXml() + { + return $this->xmlWriter->flush(false); + } +} diff --git a/library/Zend/XmlRpc/README.md b/library/Zend/XmlRpc/README.md new file mode 100755 index 0000000000..f003296a43 --- /dev/null +++ b/library/Zend/XmlRpc/README.md @@ -0,0 +1,15 @@ +XML-RPC Component from ZF2 +========================== + +This is the XML-RPC component for ZF2. + +- File issues at https://github.com/zendframework/zf2/issues +- Create pull requests against https://github.com/zendframework/zf2 +- Documentation is at http://framework.zend.com/docs + +LICENSE +------- + +The files in this archive are released under the [Zend Framework +license](http://framework.zend.com/license), which is a 3-clause BSD license. + diff --git a/library/Zend/XmlRpc/Request.php b/library/Zend/XmlRpc/Request.php new file mode 100755 index 0000000000..82a7eedbe2 --- /dev/null +++ b/library/Zend/XmlRpc/Request.php @@ -0,0 +1,444 @@ +setMethod($method); + } + + if ($params !== null) { + $this->setParams($params); + } + } + + + /** + * Set encoding to use in request + * + * @param string $encoding + * @return \Zend\XmlRpc\Request + */ + public function setEncoding($encoding) + { + $this->encoding = $encoding; + AbstractValue::setEncoding($encoding); + return $this; + } + + /** + * Retrieve current request encoding + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Set method to call + * + * @param string $method + * @return bool Returns true on success, false if method name is invalid + */ + public function setMethod($method) + { + if (!is_string($method) || !preg_match('/^[a-z0-9_.:\\\\\/]+$/i', $method)) { + $this->fault = new Fault(634, 'Invalid method name ("' . $method . '")'); + $this->fault->setEncoding($this->getEncoding()); + return false; + } + + $this->method = $method; + return true; + } + + /** + * Retrieve call method + * + * @return string + */ + public function getMethod() + { + return $this->method; + } + + /** + * Add a parameter to the parameter stack + * + * Adds a parameter to the parameter stack, associating it with the type + * $type if provided + * + * @param mixed $value + * @param string $type Optional; type hinting + * @return void + */ + public function addParam($value, $type = null) + { + $this->params[] = $value; + if (null === $type) { + // Detect type if not provided explicitly + if ($value instanceof AbstractValue) { + $type = $value->getType(); + } else { + $xmlRpcValue = AbstractValue::getXmlRpcValue($value); + $type = $xmlRpcValue->getType(); + } + } + $this->types[] = $type; + $this->xmlRpcParams[] = array('value' => $value, 'type' => $type); + } + + /** + * Set the parameters array + * + * If called with a single, array value, that array is used to set the + * parameters stack. If called with multiple values or a single non-array + * value, the arguments are used to set the parameters stack. + * + * Best is to call with array of the format, in order to allow type hinting + * when creating the XMLRPC values for each parameter: + * + * $array = array( + * array( + * 'value' => $value, + * 'type' => $type + * )[, ... ] + * ); + * + * + * @access public + * @return void + */ + public function setParams() + { + $argc = func_num_args(); + $argv = func_get_args(); + if (0 == $argc) { + return; + } + + if ((1 == $argc) && is_array($argv[0])) { + $params = array(); + $types = array(); + $wellFormed = true; + foreach ($argv[0] as $arg) { + if (!is_array($arg) || !isset($arg['value'])) { + $wellFormed = false; + break; + } + $params[] = $arg['value']; + + if (!isset($arg['type'])) { + $xmlRpcValue = AbstractValue::getXmlRpcValue($arg['value']); + $arg['type'] = $xmlRpcValue->getType(); + } + $types[] = $arg['type']; + } + if ($wellFormed) { + $this->xmlRpcParams = $argv[0]; + $this->params = $params; + $this->types = $types; + } else { + $this->params = $argv[0]; + $this->types = array(); + $xmlRpcParams = array(); + foreach ($argv[0] as $arg) { + if ($arg instanceof AbstractValue) { + $type = $arg->getType(); + } else { + $xmlRpcValue = AbstractValue::getXmlRpcValue($arg); + $type = $xmlRpcValue->getType(); + } + $xmlRpcParams[] = array('value' => $arg, 'type' => $type); + $this->types[] = $type; + } + $this->xmlRpcParams = $xmlRpcParams; + } + return; + } + + $this->params = $argv; + $this->types = array(); + $xmlRpcParams = array(); + foreach ($argv as $arg) { + if ($arg instanceof AbstractValue) { + $type = $arg->getType(); + } else { + $xmlRpcValue = AbstractValue::getXmlRpcValue($arg); + $type = $xmlRpcValue->getType(); + } + $xmlRpcParams[] = array('value' => $arg, 'type' => $type); + $this->types[] = $type; + } + $this->xmlRpcParams = $xmlRpcParams; + } + + /** + * Retrieve the array of parameters + * + * @return array + */ + public function getParams() + { + return $this->params; + } + + /** + * Return parameter types + * + * @return array + */ + public function getTypes() + { + return $this->types; + } + + /** + * Load XML and parse into request components + * + * @param string $request + * @throws Exception\ValueException if invalid XML + * @return bool True on success, false if an error occurred. + */ + public function loadXml($request) + { + if (!is_string($request)) { + $this->fault = new Fault(635); + $this->fault->setEncoding($this->getEncoding()); + return false; + } + + // @see ZF-12293 - disable external entities for security purposes + $loadEntities = libxml_disable_entity_loader(true); + $xmlErrorsFlag = libxml_use_internal_errors(true); + try { + $dom = new DOMDocument; + $dom->loadXML($request); + foreach ($dom->childNodes as $child) { + if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) { + throw new Exception\ValueException( + 'Invalid XML: Detected use of illegal DOCTYPE' + ); + } + } + ErrorHandler::start(); + $xml = simplexml_import_dom($dom); + $error = ErrorHandler::stop(); + libxml_disable_entity_loader($loadEntities); + libxml_use_internal_errors($xmlErrorsFlag); + } catch (\Exception $e) { + // Not valid XML + $this->fault = new Fault(631); + $this->fault->setEncoding($this->getEncoding()); + libxml_disable_entity_loader($loadEntities); + libxml_use_internal_errors($xmlErrorsFlag); + return false; + } + if (!$xml instanceof SimpleXMLElement || $error) { + // Not valid XML + $this->fault = new Fault(631); + $this->fault->setEncoding($this->getEncoding()); + libxml_use_internal_errors($xmlErrorsFlag); + return false; + } + + // Check for method name + if (empty($xml->methodName)) { + // Missing method name + $this->fault = new Fault(632); + $this->fault->setEncoding($this->getEncoding()); + return false; + } + + $this->method = (string) $xml->methodName; + + // Check for parameters + if (!empty($xml->params)) { + $types = array(); + $argv = array(); + foreach ($xml->params->children() as $param) { + if (!isset($param->value)) { + $this->fault = new Fault(633); + $this->fault->setEncoding($this->getEncoding()); + return false; + } + + try { + $param = AbstractValue::getXmlRpcValue($param->value, AbstractValue::XML_STRING); + $types[] = $param->getType(); + $argv[] = $param->getValue(); + } catch (\Exception $e) { + $this->fault = new Fault(636); + $this->fault->setEncoding($this->getEncoding()); + return false; + } + } + + $this->types = $types; + $this->params = $argv; + } + + $this->xml = $request; + + return true; + } + + /** + * Does the current request contain errors and should it return a fault + * response? + * + * @return bool + */ + public function isFault() + { + return $this->fault instanceof Fault; + } + + /** + * Retrieve the fault response, if any + * + * @return null|\Zend\XmlRpc\Fault + */ + public function getFault() + { + return $this->fault; + } + + /** + * Retrieve method parameters as XMLRPC values + * + * @return array + */ + protected function _getXmlRpcParams() + { + $params = array(); + if (is_array($this->xmlRpcParams)) { + foreach ($this->xmlRpcParams as $param) { + $value = $param['value']; + $type = $param['type'] ?: AbstractValue::AUTO_DETECT_TYPE; + + if (!$value instanceof AbstractValue) { + $value = AbstractValue::getXmlRpcValue($value, $type); + } + $params[] = $value; + } + } + + return $params; + } + + /** + * Create XML request + * + * @return string + */ + public function saveXml() + { + $args = $this->_getXmlRpcParams(); + $method = $this->getMethod(); + + $generator = AbstractValue::getGenerator(); + $generator->openElement('methodCall') + ->openElement('methodName', $method) + ->closeElement('methodName'); + + if (is_array($args) && count($args)) { + $generator->openElement('params'); + + foreach ($args as $arg) { + $generator->openElement('param'); + $arg->generateXml(); + $generator->closeElement('param'); + } + $generator->closeElement('params'); + } + $generator->closeElement('methodCall'); + + return $generator->flush(); + } + + /** + * Return XML request + * + * @return string + */ + public function __toString() + { + return $this->saveXML(); + } +} diff --git a/library/Zend/XmlRpc/Request/Http.php b/library/Zend/XmlRpc/Request/Http.php new file mode 100755 index 0000000000..d42a8331dd --- /dev/null +++ b/library/Zend/XmlRpc/Request/Http.php @@ -0,0 +1,108 @@ +fault = new Fault(630); + return; + } + + $this->xml = $xml; + + $this->loadXml($xml); + } + + /** + * Retrieve the raw XML request + * + * @return string + */ + public function getRawRequest() + { + return $this->xml; + } + + /** + * Get headers + * + * Gets all headers as key => value pairs and returns them. + * + * @return array + */ + public function getHeaders() + { + if (null === $this->headers) { + $this->headers = array(); + foreach ($_SERVER as $key => $value) { + if ('HTTP_' == substr($key, 0, 5)) { + $header = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5))))); + $this->headers[$header] = $value; + } + } + } + + return $this->headers; + } + + /** + * Retrieve the full HTTP request, including headers and XML + * + * @return string + */ + public function getFullRequest() + { + $request = ''; + foreach ($this->getHeaders() as $key => $value) { + $request .= $key . ': ' . $value . "\n"; + } + + $request .= $this->xml; + + return $request; + } +} diff --git a/library/Zend/XmlRpc/Request/Stdin.php b/library/Zend/XmlRpc/Request/Stdin.php new file mode 100755 index 0000000000..bcf748e95c --- /dev/null +++ b/library/Zend/XmlRpc/Request/Stdin.php @@ -0,0 +1,66 @@ +fault = new Fault(630); + return; + } + + $xml = ''; + while (!feof($fh)) { + $xml .= fgets($fh); + } + fclose($fh); + + $this->xml = $xml; + + $this->loadXml($xml); + } + + /** + * Retrieve the raw XML request + * + * @return string + */ + public function getRawRequest() + { + return $this->xml; + } +} diff --git a/library/Zend/XmlRpc/Response.php b/library/Zend/XmlRpc/Response.php new file mode 100755 index 0000000000..f8537584e4 --- /dev/null +++ b/library/Zend/XmlRpc/Response.php @@ -0,0 +1,224 @@ +setReturnValue($return, $type); + } + + /** + * Set encoding to use in response + * + * @param string $encoding + * @return \Zend\XmlRpc\Response + */ + public function setEncoding($encoding) + { + $this->encoding = $encoding; + AbstractValue::setEncoding($encoding); + return $this; + } + + /** + * Retrieve current response encoding + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Set the return value + * + * Sets the return value, with optional type hinting if provided. + * + * @param mixed $value + * @param string $type + * @return void + */ + public function setReturnValue($value, $type = null) + { + $this->return = $value; + $this->type = (string) $type; + } + + /** + * Retrieve the return value + * + * @return mixed + */ + public function getReturnValue() + { + return $this->return; + } + + /** + * Retrieve the XMLRPC value for the return value + * + * @return \Zend\XmlRpc\AbstractValue + */ + protected function _getXmlRpcReturn() + { + return AbstractValue::getXmlRpcValue($this->return); + } + + /** + * Is the response a fault response? + * + * @return bool + */ + public function isFault() + { + return $this->fault instanceof Fault; + } + + /** + * Returns the fault, if any. + * + * @return null|\Zend\XmlRpc\Fault + */ + public function getFault() + { + return $this->fault; + } + + /** + * Load a response from an XML response + * + * Attempts to load a response from an XMLRPC response, autodetecting if it + * is a fault response. + * + * @param string $response + * @throws Exception\ValueException if invalid XML + * @return bool True if a valid XMLRPC response, false if a fault + * response or invalid input + */ + public function loadXml($response) + { + if (!is_string($response)) { + $this->fault = new Fault(650); + $this->fault->setEncoding($this->getEncoding()); + return false; + } + + try { + $xml = XmlSecurity::scan($response); + } catch (\ZendXml\Exception\RuntimeException $e) { + $this->fault = new Fault(651); + $this->fault->setEncoding($this->getEncoding()); + return false; + } + + if (!empty($xml->fault)) { + // fault response + $this->fault = new Fault(); + $this->fault->setEncoding($this->getEncoding()); + $this->fault->loadXml($response); + return false; + } + + if (empty($xml->params)) { + // Invalid response + $this->fault = new Fault(652); + $this->fault->setEncoding($this->getEncoding()); + return false; + } + + try { + if (!isset($xml->params) || !isset($xml->params->param) || !isset($xml->params->param->value)) { + throw new Exception\ValueException('Missing XML-RPC value in XML'); + } + $valueXml = $xml->params->param->value->asXML(); + $value = AbstractValue::getXmlRpcValue($valueXml, AbstractValue::XML_STRING); + } catch (Exception\ValueException $e) { + $this->fault = new Fault(653); + $this->fault->setEncoding($this->getEncoding()); + return false; + } + + $this->setReturnValue($value->getValue()); + return true; + } + + /** + * Return response as XML + * + * @return string + */ + public function saveXml() + { + $value = $this->_getXmlRpcReturn(); + $generator = AbstractValue::getGenerator(); + $generator->openElement('methodResponse') + ->openElement('params') + ->openElement('param'); + $value->generateXml(); + $generator->closeElement('param') + ->closeElement('params') + ->closeElement('methodResponse'); + + return $generator->flush(); + } + + /** + * Return XML response + * + * @return string + */ + public function __toString() + { + return $this->saveXML(); + } +} diff --git a/library/Zend/XmlRpc/Response/Http.php b/library/Zend/XmlRpc/Response/Http.php new file mode 100755 index 0000000000..00ae07cc9c --- /dev/null +++ b/library/Zend/XmlRpc/Response/Http.php @@ -0,0 +1,32 @@ +getEncoding())); + } + + return parent::__toString(); + } +} diff --git a/library/Zend/XmlRpc/Server.php b/library/Zend/XmlRpc/Server.php new file mode 100755 index 0000000000..e2cfcea6b3 --- /dev/null +++ b/library/Zend/XmlRpc/Server.php @@ -0,0 +1,609 @@ + + * use Zend\XmlRpc; + * + * // Instantiate server + * $server = new XmlRpc\Server(); + * + * // Allow some exceptions to report as fault responses: + * XmlRpc\Server\Fault::attachFaultException('My\\Exception'); + * XmlRpc\Server\Fault::attachObserver('My\\Fault\\Observer'); + * + * // Get or build dispatch table: + * if (!XmlRpc\Server\Cache::get($filename, $server)) { + * + * // Attach Some_Service_Class in 'some' namespace + * $server->setClass('Some\\Service\\Class', 'some'); + * + * // Attach Another_Service_Class in 'another' namespace + * $server->setClass('Another\\Service\\Class', 'another'); + * + * // Create dispatch table cache file + * XmlRpc\Server\Cache::save($filename, $server); + * } + * + * $response = $server->handle(); + * echo $response; + * + */ +class Server extends AbstractServer +{ + /** + * Character encoding + * @var string + */ + protected $encoding = 'UTF-8'; + + /** + * Request processed + * @var null|Request + */ + protected $request = null; + + /** + * Class to use for responses; defaults to {@link Response\Http} + * @var string + */ + protected $responseClass = 'Zend\XmlRpc\Response\Http'; + + /** + * Dispatch table of name => method pairs + * @var Definition + */ + protected $table; + + /** + * PHP types => XML-RPC types + * @var array + */ + protected $typeMap = array( + 'i4' => 'i4', + 'int' => 'int', + 'integer' => 'int', + 'i8' => 'i8', + 'ex:i8' => 'i8', + 'double' => 'double', + 'float' => 'double', + 'real' => 'double', + 'boolean' => 'boolean', + 'bool' => 'boolean', + 'true' => 'boolean', + 'false' => 'boolean', + 'string' => 'string', + 'str' => 'string', + 'base64' => 'base64', + 'dateTime.iso8601' => 'dateTime.iso8601', + 'date' => 'dateTime.iso8601', + 'time' => 'dateTime.iso8601', + 'DateTime' => 'dateTime.iso8601', + 'array' => 'array', + 'struct' => 'struct', + 'null' => 'nil', + 'nil' => 'nil', + 'ex:nil' => 'nil', + 'void' => 'void', + 'mixed' => 'struct', + ); + + /** + * Send arguments to all methods or just constructor? + * + * @var bool + */ + protected $sendArgumentsToAllMethods = true; + + /** + * Flag: whether or not {@link handle()} should return a response instead + * of automatically emitting it. + * @var bool + */ + protected $returnResponse = false; + + /** + * Last response results. + * @var Response + */ + protected $response; + + /** + * Constructor + * + * Creates system.* methods. + * + */ + public function __construct() + { + $this->table = new Definition(); + $this->registerSystemMethods(); + } + + /** + * Proxy calls to system object + * + * @param string $method + * @param array $params + * @return mixed + * @throws Server\Exception\BadMethodCallException + */ + public function __call($method, $params) + { + $system = $this->getSystem(); + if (!method_exists($system, $method)) { + throw new Server\Exception\BadMethodCallException('Unknown instance method called on server: ' . $method); + } + return call_user_func_array(array($system, $method), $params); + } + + /** + * Attach a callback as an XMLRPC method + * + * Attaches a callback as an XMLRPC method, prefixing the XMLRPC method name + * with $namespace, if provided. Reflection is done on the callback's + * docblock to create the methodHelp for the XMLRPC method. + * + * Additional arguments to pass to the function at dispatch may be passed; + * any arguments following the namespace will be aggregated and passed at + * dispatch time. + * + * @param string|array|callable $function Valid callback + * @param string $namespace Optional namespace prefix + * @throws Server\Exception\InvalidArgumentException + * @return void + */ + public function addFunction($function, $namespace = '') + { + if (!is_string($function) && !is_array($function)) { + throw new Server\Exception\InvalidArgumentException('Unable to attach function; invalid', 611); + } + + $argv = null; + if (2 < func_num_args()) { + $argv = func_get_args(); + $argv = array_slice($argv, 2); + } + + $function = (array) $function; + foreach ($function as $func) { + if (!is_string($func) || !function_exists($func)) { + throw new Server\Exception\InvalidArgumentException('Unable to attach function; invalid', 611); + } + $reflection = Reflection::reflectFunction($func, $argv, $namespace); + $this->_buildSignature($reflection); + } + } + + /** + * Attach class methods as XMLRPC method handlers + * + * $class may be either a class name or an object. Reflection is done on the + * class or object to determine the available public methods, and each is + * attached to the server as an available method; if a $namespace has been + * provided, that namespace is used to prefix the XMLRPC method names. + * + * Any additional arguments beyond $namespace will be passed to a method at + * invocation. + * + * @param string|object $class + * @param string $namespace Optional + * @param mixed $argv Optional arguments to pass to methods + * @return void + * @throws Server\Exception\InvalidArgumentException on invalid input + */ + public function setClass($class, $namespace = '', $argv = null) + { + if (is_string($class) && !class_exists($class)) { + throw new Server\Exception\InvalidArgumentException('Invalid method class', 610); + } + + if (2 < func_num_args()) { + $argv = func_get_args(); + $argv = array_slice($argv, 2); + } + + $dispatchable = Reflection::reflectClass($class, $argv, $namespace); + foreach ($dispatchable->getMethods() as $reflection) { + $this->_buildSignature($reflection, $class); + } + } + + /** + * Raise an xmlrpc server fault + * + * @param string|\Exception $fault + * @param int $code + * @return Server\Fault + */ + public function fault($fault = null, $code = 404) + { + if (!$fault instanceof \Exception) { + $fault = (string) $fault; + if (empty($fault)) { + $fault = 'Unknown Error'; + } + $fault = new Server\Exception\RuntimeException($fault, $code); + } + + return Server\Fault::getInstance($fault); + } + + /** + * Set return response flag + * + * If true, {@link handle()} will return the response instead of + * automatically sending it back to the requesting client. + * + * The response is always available via {@link getResponse()}. + * + * @param bool $flag + * @return Server + */ + public function setReturnResponse($flag = true) + { + $this->returnResponse = ($flag) ? true : false; + return $this; + } + + /** + * Retrieve return response flag + * + * @return bool + */ + public function getReturnResponse() + { + return $this->returnResponse; + } + + /** + * Handle an xmlrpc call + * + * @param Request $request Optional + * @return Response|Fault + */ + public function handle($request = false) + { + // Get request + if ((!$request || !$request instanceof Request) + && (null === ($request = $this->getRequest())) + ) { + $request = new Request\Http(); + $request->setEncoding($this->getEncoding()); + } + + $this->setRequest($request); + + if ($request->isFault()) { + $response = $request->getFault(); + } else { + try { + $response = $this->handleRequest($request); + } catch (\Exception $e) { + $response = $this->fault($e); + } + } + + // Set output encoding + $response->setEncoding($this->getEncoding()); + $this->response = $response; + + if (!$this->returnResponse) { + echo $response; + return; + } + + return $response; + } + + /** + * Load methods as returned from {@link getFunctions} + * + * Typically, you will not use this method; it will be called using the + * results pulled from {@link Zend\XmlRpc\Server\Cache::get()}. + * + * @param array|Definition $definition + * @return void + * @throws Server\Exception\InvalidArgumentException on invalid input + */ + public function loadFunctions($definition) + { + if (!is_array($definition) && (!$definition instanceof Definition)) { + if (is_object($definition)) { + $type = get_class($definition); + } else { + $type = gettype($definition); + } + throw new Server\Exception\InvalidArgumentException('Unable to load server definition; must be an array or Zend\Server\Definition, received ' . $type, 612); + } + + $this->table->clearMethods(); + $this->registerSystemMethods(); + + if ($definition instanceof Definition) { + $definition = $definition->getMethods(); + } + + foreach ($definition as $key => $method) { + if ('system.' == substr($key, 0, 7)) { + continue; + } + $this->table->addMethod($method, $key); + } + } + + /** + * Set encoding + * + * @param string $encoding + * @return Server + */ + public function setEncoding($encoding) + { + $this->encoding = $encoding; + AbstractValue::setEncoding($encoding); + return $this; + } + + /** + * Retrieve current encoding + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Do nothing; persistence is handled via {@link Zend\XmlRpc\Server\Cache} + * + * @param mixed $mode + * @return void + */ + public function setPersistence($mode) + { + } + + /** + * Set the request object + * + * @param string|Request $request + * @return Server + * @throws Server\Exception\InvalidArgumentException on invalid request class or object + */ + public function setRequest($request) + { + if (is_string($request) && class_exists($request)) { + $request = new $request(); + if (!$request instanceof Request) { + throw new Server\Exception\InvalidArgumentException('Invalid request class'); + } + $request->setEncoding($this->getEncoding()); + } elseif (!$request instanceof Request) { + throw new Server\Exception\InvalidArgumentException('Invalid request object'); + } + + $this->request = $request; + return $this; + } + + /** + * Return currently registered request object + * + * @return null|Request + */ + public function getRequest() + { + return $this->request; + } + + /** + * Last response. + * + * @return Response + */ + public function getResponse() + { + return $this->response; + } + + /** + * Set the class to use for the response + * + * @param string $class + * @throws Server\Exception\InvalidArgumentException if invalid response class + * @return bool True if class was set, false if not + */ + public function setResponseClass($class) + { + if (!class_exists($class) || !static::isSubclassOf($class, 'Zend\XmlRpc\Response')) { + throw new Server\Exception\InvalidArgumentException('Invalid response class'); + } + $this->responseClass = $class; + return true; + } + + /** + * Retrieve current response class + * + * @return string + */ + public function getResponseClass() + { + return $this->responseClass; + } + + /** + * Retrieve dispatch table + * + * @return array + */ + public function getDispatchTable() + { + return $this->table; + } + + /** + * Returns a list of registered methods + * + * Returns an array of dispatchables (Zend\Server\Reflection\ReflectionFunction, + * ReflectionMethod, and ReflectionClass items). + * + * @return array + */ + public function getFunctions() + { + return $this->table->toArray(); + } + + /** + * Retrieve system object + * + * @return Server\System + */ + public function getSystem() + { + return $this->system; + } + + /** + * Send arguments to all methods? + * + * If setClass() is used to add classes to the server, this flag defined + * how to handle arguments. If set to true, all methods including constructor + * will receive the arguments. If set to false, only constructor will receive the + * arguments + */ + public function sendArgumentsToAllMethods($flag = null) + { + if ($flag === null) { + return $this->sendArgumentsToAllMethods; + } + + $this->sendArgumentsToAllMethods = (bool) $flag; + return $this; + } + + /** + * Map PHP type to XML-RPC type + * + * @param string $type + * @return string + */ + protected function _fixType($type) + { + if (isset($this->typeMap[$type])) { + return $this->typeMap[$type]; + } + return 'void'; + } + + /** + * Handle an xmlrpc call (actual work) + * + * @param Request $request + * @return Response + * @throws Server\Exception\RuntimeException + * Zend\XmlRpc\Server\Exceptions are thrown for internal errors; otherwise, + * any other exception may be thrown by the callback + */ + protected function handleRequest(Request $request) + { + $method = $request->getMethod(); + + // Check for valid method + if (!$this->table->hasMethod($method)) { + throw new Server\Exception\RuntimeException('Method "' . $method . '" does not exist', 620); + } + + $info = $this->table->getMethod($method); + $params = $request->getParams(); + $argv = $info->getInvokeArguments(); + if (0 < count($argv) and $this->sendArgumentsToAllMethods()) { + $params = array_merge($params, $argv); + } + + // Check calling parameters against signatures + $matched = false; + $sigCalled = $request->getTypes(); + + $sigLength = count($sigCalled); + $paramsLen = count($params); + if ($sigLength < $paramsLen) { + for ($i = $sigLength; $i < $paramsLen; ++$i) { + $xmlRpcValue = AbstractValue::getXmlRpcValue($params[$i]); + $sigCalled[] = $xmlRpcValue->getType(); + } + } + + $signatures = $info->getPrototypes(); + foreach ($signatures as $signature) { + $sigParams = $signature->getParameters(); + if ($sigCalled === $sigParams) { + $matched = true; + break; + } + } + if (!$matched) { + throw new Server\Exception\RuntimeException('Calling parameters do not match signature', 623); + } + + $return = $this->_dispatch($info, $params); + $responseClass = $this->getResponseClass(); + return new $responseClass($return); + } + + /** + * Register system methods with the server + * + * @return void + */ + protected function registerSystemMethods() + { + $system = new Server\System($this); + $this->system = $system; + $this->setClass($system, 'system'); + } + + /** + * Checks if the object has this class as one of its parents + * + * @see https://bugs.php.net/bug.php?id=53727 + * @see https://github.com/zendframework/zf2/pull/1807 + * + * @param string $className + * @param string $type + * @return bool + */ + protected static function isSubclassOf($className, $type) + { + if (is_subclass_of($className, $type)) { + return true; + } + if (PHP_VERSION_ID >= 50307) { + return false; + } + if (!interface_exists($type)) { + return false; + } + $r = new ReflectionClass($className); + return $r->implementsInterface($type); + } +} diff --git a/library/Zend/XmlRpc/Server/Cache.php b/library/Zend/XmlRpc/Server/Cache.php new file mode 100755 index 0000000000..f4e643ea6d --- /dev/null +++ b/library/Zend/XmlRpc/Server/Cache.php @@ -0,0 +1,26 @@ + true); + + /** + * @var array Array of fault observers + */ + protected static $observers = array(); + + /** + * Constructor + * + * @param \Exception $e + * @return Fault + */ + public function __construct(\Exception $e) + { + $this->exception = $e; + $code = 404; + $message = 'Unknown error'; + + foreach (array_keys(static::$faultExceptionClasses) as $class) { + if ($e instanceof $class) { + $code = $e->getCode(); + $message = $e->getMessage(); + break; + } + } + + parent::__construct($code, $message); + + // Notify exception observers, if present + if (!empty(static::$observers)) { + foreach (array_keys(static::$observers) as $observer) { + $observer::observe($this); + } + } + } + + /** + * Return Zend\XmlRpc\Server\Fault instance + * + * @param \Exception $e + * @return Fault + */ + public static function getInstance(\Exception $e) + { + return new static($e); + } + + /** + * Attach valid exceptions that can be used to define xmlrpc faults + * + * @param string|array $classes Class name or array of class names + * @return void + */ + public static function attachFaultException($classes) + { + if (!is_array($classes)) { + $classes = (array) $classes; + } + + foreach ($classes as $class) { + if (is_string($class) && class_exists($class)) { + static::$faultExceptionClasses[$class] = true; + } + } + } + + /** + * Detach fault exception classes + * + * @param string|array $classes Class name or array of class names + * @return void + */ + public static function detachFaultException($classes) + { + if (!is_array($classes)) { + $classes = (array) $classes; + } + + foreach ($classes as $class) { + if (is_string($class) && isset(static::$faultExceptionClasses[$class])) { + unset(static::$faultExceptionClasses[$class]); + } + } + } + + /** + * Attach an observer class + * + * Allows observation of xmlrpc server faults, thus allowing logging or mail + * notification of fault responses on the xmlrpc server. + * + * Expects a valid class name; that class must have a public static method + * 'observe' that accepts an exception as its sole argument. + * + * @param string $class + * @return bool + */ + public static function attachObserver($class) + { + if (!is_string($class) + || !class_exists($class) + || !is_callable(array($class, 'observe')) + ) { + return false; + } + + if (!isset(static::$observers[$class])) { + static::$observers[$class] = true; + } + + return true; + } + + /** + * Detach an observer + * + * @param string $class + * @return bool + */ + public static function detachObserver($class) + { + if (!isset(static::$observers[$class])) { + return false; + } + + unset(static::$observers[$class]); + return true; + } + + /** + * Retrieve the exception + * + * @access public + * @return \Exception + */ + public function getException() + { + return $this->exception; + } +} diff --git a/library/Zend/XmlRpc/Server/System.php b/library/Zend/XmlRpc/Server/System.php new file mode 100755 index 0000000000..8a57838b1c --- /dev/null +++ b/library/Zend/XmlRpc/Server/System.php @@ -0,0 +1,144 @@ +server = $server; + } + + /** + * List all available XMLRPC methods + * + * Returns an array of methods. + * + * @return array + */ + public function listMethods() + { + $table = $this->server->getDispatchTable()->getMethods(); + return array_keys($table); + } + + /** + * Display help message for an XMLRPC method + * + * @param string $method + * @throws Exception\InvalidArgumentException + * @return string + */ + public function methodHelp($method) + { + $table = $this->server->getDispatchTable(); + if (!$table->hasMethod($method)) { + throw new Exception\InvalidArgumentException('Method "' . $method . '" does not exist', 640); + } + + return $table->getMethod($method)->getMethodHelp(); + } + + /** + * Return a method signature + * + * @param string $method + * @throws Exception\InvalidArgumentException + * @return array + */ + public function methodSignature($method) + { + $table = $this->server->getDispatchTable(); + if (!$table->hasMethod($method)) { + throw new Exception\InvalidArgumentException('Method "' . $method . '" does not exist', 640); + } + $method = $table->getMethod($method)->toArray(); + return $method['prototypes']; + } + + /** + * Multicall - boxcar feature of XML-RPC for calling multiple methods + * in a single request. + * + * Expects an array of structs representing method calls, each element + * having the keys: + * - methodName + * - params + * + * Returns an array of responses, one for each method called, with the value + * returned by the method. If an error occurs for a given method, returns a + * struct with a fault response. + * + * @see http://www.xmlrpc.com/discuss/msgReader$1208 + * @param array $methods + * @return array + */ + public function multicall($methods) + { + $responses = array(); + foreach ($methods as $method) { + $fault = false; + if (!is_array($method)) { + $fault = $this->server->fault('system.multicall expects each method to be a struct', 601); + } elseif (!isset($method['methodName'])) { + $fault = $this->server->fault('Missing methodName: ' . var_export($methods, 1), 602); + } elseif (!isset($method['params'])) { + $fault = $this->server->fault('Missing params', 603); + } elseif (!is_array($method['params'])) { + $fault = $this->server->fault('Params must be an array', 604); + } else { + if ('system.multicall' == $method['methodName']) { + // don't allow recursive calls to multicall + $fault = $this->server->fault('Recursive system.multicall forbidden', 605); + } + } + + if (!$fault) { + try { + $request = new \Zend\XmlRpc\Request(); + $request->setMethod($method['methodName']); + $request->setParams($method['params']); + $response = $this->server->handle($request); + if ($response instanceof \Zend\XmlRpc\Fault + || $response->isFault() + ) { + $fault = $response; + } else { + $responses[] = $response->getReturnValue(); + } + } catch (\Exception $e) { + $fault = $this->server->fault($e); + } + } + + if ($fault) { + $responses[] = array( + 'faultCode' => $fault->getCode(), + 'faultString' => $fault->getMessage() + ); + } + } + + return $responses; + } +} diff --git a/library/Zend/XmlRpc/Value/AbstractCollection.php b/library/Zend/XmlRpc/Value/AbstractCollection.php new file mode 100755 index 0000000000..ed4f8193f3 --- /dev/null +++ b/library/Zend/XmlRpc/Value/AbstractCollection.php @@ -0,0 +1,48 @@ + $value) { + // If the elements of the given array are not Zend\XmlRpc\Value objects, + // we need to convert them as such (using auto-detection from PHP value) + if (!$value instanceof parent) { + $value = static::getXmlRpcValue($value, self::AUTO_DETECT_TYPE); + } + $this->value[$key] = $value; + } + } + + + /** + * Return the value of this object, convert the XML-RPC native collection values into a PHP array + * + * @return array + */ + public function getValue() + { + $values = (array) $this->value; + foreach ($values as $key => $value) { + $values[$key] = $value->getValue(); + } + return $values; + } +} diff --git a/library/Zend/XmlRpc/Value/AbstractScalar.php b/library/Zend/XmlRpc/Value/AbstractScalar.php new file mode 100755 index 0000000000..3e7b8eacb8 --- /dev/null +++ b/library/Zend/XmlRpc/Value/AbstractScalar.php @@ -0,0 +1,30 @@ +getGenerator(); + + $generator->openElement('value') + ->openElement($this->type, $this->value) + ->closeElement($this->type) + ->closeElement('value'); + } +} diff --git a/library/Zend/XmlRpc/Value/ArrayValue.php b/library/Zend/XmlRpc/Value/ArrayValue.php new file mode 100755 index 0000000000..99d77437b3 --- /dev/null +++ b/library/Zend/XmlRpc/Value/ArrayValue.php @@ -0,0 +1,47 @@ +type = self::XMLRPC_TYPE_ARRAY; + parent::__construct($value); + } + + + /** + * Generate the XML code that represent an array native MXL-RPC value + * + * @return void + */ + protected function _generateXml() + { + $generator = $this->getGenerator(); + $generator->openElement('value') + ->openElement('array') + ->openElement('data'); + + if (is_array($this->value)) { + foreach ($this->value as $val) { + $val->generateXml(); + } + } + $generator->closeElement('data') + ->closeElement('array') + ->closeElement('value'); + } +} diff --git a/library/Zend/XmlRpc/Value/Base64.php b/library/Zend/XmlRpc/Value/Base64.php new file mode 100755 index 0000000000..9ba44aceab --- /dev/null +++ b/library/Zend/XmlRpc/Value/Base64.php @@ -0,0 +1,42 @@ +type = self::XMLRPC_TYPE_BASE64; + + $value = (string) $value; // Make sure this value is string + if (!$alreadyEncoded) { + $value = base64_encode($value); // We encode it in base64 + } + $this->value = $value; + } + + /** + * Return the value of this object, convert the XML-RPC native base64 value into a PHP string + * We return this value decoded (a normal string) + * + * @return string + */ + public function getValue() + { + return base64_decode($this->value); + } +} diff --git a/library/Zend/XmlRpc/Value/BigInteger.php b/library/Zend/XmlRpc/Value/BigInteger.php new file mode 100755 index 0000000000..e85ef95371 --- /dev/null +++ b/library/Zend/XmlRpc/Value/BigInteger.php @@ -0,0 +1,34 @@ +value = BigIntegerMath::factory()->init($value, 10); + $this->type = self::XMLRPC_TYPE_I8; + } + + /** + * Return bigint value object + * + * @return string + */ + public function getValue() + { + return $this->value; + } +} diff --git a/library/Zend/XmlRpc/Value/Boolean.php b/library/Zend/XmlRpc/Value/Boolean.php new file mode 100755 index 0000000000..5ec6b7932b --- /dev/null +++ b/library/Zend/XmlRpc/Value/Boolean.php @@ -0,0 +1,37 @@ +type = self::XMLRPC_TYPE_BOOLEAN; + // Make sure the value is boolean and then convert it into an integer + // The double conversion is because a bug in the ZendOptimizer in PHP version 5.0.4 + $this->value = (int)(bool) $value; + } + + /** + * Return the value of this object, convert the XML-RPC native boolean value into a PHP boolean + * + * @return bool + */ + public function getValue() + { + return (bool) $this->value; + } +} diff --git a/library/Zend/XmlRpc/Value/DateTime.php b/library/Zend/XmlRpc/Value/DateTime.php new file mode 100755 index 0000000000..9ec7253e6d --- /dev/null +++ b/library/Zend/XmlRpc/Value/DateTime.php @@ -0,0 +1,67 @@ +type = self::XMLRPC_TYPE_DATETIME; + + if ($value instanceof \DateTime) { + $this->value = $value->format($this->phpFormatString); + } elseif (is_numeric($value)) { // The value is numeric, we make sure it is an integer + $this->value = date($this->phpFormatString, (int) $value); + } else { + try { + $dateTime = new \DateTime($value); + } catch (\Exception $e) { + throw new Exception\ValueException($e->getMessage(), $e->getCode(), $e); + } + + $this->value = $dateTime->format($this->phpFormatString); // Convert the DateTime to iso8601 format + } + } + + /** + * Return the value of this object as iso8601 dateTime value + * + * @return int As a Unix timestamp + */ + public function getValue() + { + return $this->value; + } +} diff --git a/library/Zend/XmlRpc/Value/Double.php b/library/Zend/XmlRpc/Value/Double.php new file mode 100755 index 0000000000..722012000f --- /dev/null +++ b/library/Zend/XmlRpc/Value/Double.php @@ -0,0 +1,36 @@ +type = self::XMLRPC_TYPE_DOUBLE; + $precision = (int) ini_get('precision'); + $formatString = '%1.' . $precision . 'F'; + $this->value = rtrim(sprintf($formatString, (float) $value), '0'); + } + + /** + * Return the value of this object, convert the XML-RPC native double value into a PHP float + * + * @return float + */ + public function getValue() + { + return (float) $this->value; + } +} diff --git a/library/Zend/XmlRpc/Value/Integer.php b/library/Zend/XmlRpc/Value/Integer.php new file mode 100755 index 0000000000..40d9386649 --- /dev/null +++ b/library/Zend/XmlRpc/Value/Integer.php @@ -0,0 +1,41 @@ + PHP_INT_MAX) { + throw new Exception\ValueException('Overlong integer given'); + } + + $this->type = self::XMLRPC_TYPE_INTEGER; + $this->value = (int) $value; // Make sure this value is integer + } + + /** + * Return the value of this object, convert the XML-RPC native integer value into a PHP integer + * + * @return int + */ + public function getValue() + { + return $this->value; + } +} diff --git a/library/Zend/XmlRpc/Value/Nil.php b/library/Zend/XmlRpc/Value/Nil.php new file mode 100755 index 0000000000..49f3c7511c --- /dev/null +++ b/library/Zend/XmlRpc/Value/Nil.php @@ -0,0 +1,33 @@ +type = self::XMLRPC_TYPE_NIL; + $this->value = null; + } + + /** + * Return the value of this object, convert the XML-RPC native nill value into a PHP NULL + * + * @return null + */ + public function getValue() + { + return null; + } +} diff --git a/library/Zend/XmlRpc/Value/String.php b/library/Zend/XmlRpc/Value/String.php new file mode 100755 index 0000000000..66fa441b00 --- /dev/null +++ b/library/Zend/XmlRpc/Value/String.php @@ -0,0 +1,36 @@ +type = self::XMLRPC_TYPE_STRING; + + // Make sure this value is string and all XML characters are encoded + $this->value = (string) $value; + } + + /** + * Return the value of this object, convert the XML-RPC native string value into a PHP string + * + * @return string + */ + public function getValue() + { + return (string) $this->value; + } +} diff --git a/library/Zend/XmlRpc/Value/Struct.php b/library/Zend/XmlRpc/Value/Struct.php new file mode 100755 index 0000000000..99d55bb847 --- /dev/null +++ b/library/Zend/XmlRpc/Value/Struct.php @@ -0,0 +1,49 @@ +type = self::XMLRPC_TYPE_STRUCT; + parent::__construct($value); + } + + + /** + * Generate the XML code that represent struct native MXL-RPC value + * + * @return void + */ + protected function _generateXML() + { + $generator = $this->getGenerator(); + $generator->openElement('value') + ->openElement('struct'); + + if (is_array($this->value)) { + foreach ($this->value as $name => $val) { + $generator->openElement('member') + ->openElement('name', $name) + ->closeElement('name'); + $val->generateXml(); + $generator->closeElement('member'); + } + } + $generator->closeElement('struct') + ->closeElement('value'); + } +} diff --git a/library/Zend/XmlRpc/composer.json b/library/Zend/XmlRpc/composer.json new file mode 100755 index 0000000000..1520a41e41 --- /dev/null +++ b/library/Zend/XmlRpc/composer.json @@ -0,0 +1,30 @@ +{ + "name": "zendframework/zend-xmlrpc", + "description": " ", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "xmlrpc" + ], + "homepage": "https://github.com/zendframework/zf2", + "autoload": { + "psr-0": { + "Zend\\XmlRpc\\": "" + } + }, + "target-dir": "Zend/XmlRpc", + "require": { + "php": ">=5.3.23", + "zendframework/zend-http": "self.version", + "zendframework/zend-math": "self.version", + "zendframework/zend-server": "self.version", + "zendframework/zend-stdlib": "self.version", + "zendframework/zendxml": "1.*" + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev", + "dev-develop": "2.4-dev" + } + } +}