From 1ce18936c294d5dec27117404f82a710e4448b1b Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 31 Aug 2012 14:44:59 -0500 Subject: [PATCH] [zendframework/zf2#2284][ZF2-507] Updated README - Notice about Date header --- .coveralls.yml | 3 + .gitattributes | 6 + .gitignore | 14 + .php_cs | 43 ++ .travis.yml | 35 ++ CONTRIBUTING.md | 229 ++++++++ LICENSE.txt | 27 + README.md | 9 + composer.json | 45 ++ phpunit.xml.dist | 41 ++ phpunit.xml.travis | 41 ++ src/AbstractFilter.php | 117 ++++ src/AbstractUnicode.php | 64 +++ src/BaseName.php | 31 + src/Boolean.php | 301 ++++++++++ src/Callback.php | 105 ++++ src/Compress.php | 208 +++++++ src/Compress/AbstractCompressionAlgorithm.php | 81 +++ src/Compress/Bz2.php | 170 ++++++ .../CompressionAlgorithmInterface.php | 43 ++ src/Compress/Gz.php | 211 +++++++ src/Compress/Lzf.php | 79 +++ src/Compress/Rar.php | 235 ++++++++ src/Compress/Tar.php | 236 ++++++++ src/Compress/Zip.php | 317 ++++++++++ src/Decompress.php | 33 ++ src/Decrypt.php | 33 ++ src/Digits.php | 42 ++ src/Dir.php | 31 + src/Encrypt.php | 125 ++++ src/Encrypt/BlockCipher.php | 268 +++++++++ src/Encrypt/EncryptionAlgorithmInterface.php | 43 ++ src/Encrypt/Openssl.php | 473 +++++++++++++++ src/Exception/BadMethodCallException.php | 15 + src/Exception/DomainException.php | 20 + src/Exception/ExceptionInterface.php | 19 + src/Exception/ExtensionNotLoadedException.php | 15 + src/Exception/InvalidArgumentException.php | 15 + src/Exception/RuntimeException.php | 15 + src/File/Decrypt.php | 91 +++ src/File/Encrypt.php | 91 +++ src/File/LowerCase.php | 56 ++ src/File/Rename.php | 284 +++++++++ src/File/UpperCase.php | 56 ++ src/FilterChain.php | 227 ++++++++ src/FilterInterface.php | 27 + src/FilterPluginManager.php | 119 ++++ src/HtmlEntities.php | 201 +++++++ src/Inflector.php | 475 +++++++++++++++ src/Int.php | 31 + src/Null.php | 183 ++++++ src/PregReplace.php | 136 +++++ src/RealPath.php | 120 ++++ src/StaticFilter.php | 75 +++ src/StringToLower.php | 60 ++ src/StringToUpper.php | 60 ++ src/StringTrim.php | 114 ++++ src/StripNewlines.php | 32 ++ src/StripTags.php | 293 ++++++++++ src/Word/AbstractSeparator.php | 67 +++ src/Word/CamelCaseToDash.php | 27 + src/Word/CamelCaseToSeparator.php | 37 ++ src/Word/CamelCaseToUnderscore.php | 27 + src/Word/DashToCamelCase.php | 27 + src/Word/DashToSeparator.php | 31 + src/Word/DashToUnderscore.php | 28 + src/Word/SeparatorToCamelCase.php | 44 ++ src/Word/SeparatorToDash.php | 28 + src/Word/SeparatorToSeparator.php | 113 ++++ src/Word/UnderscoreToCamelCase.php | 27 + src/Word/UnderscoreToDash.php | 27 + src/Word/UnderscoreToSeparator.php | 28 + test/BaseNameTest.php | 39 ++ test/BooleanTest.php | 478 ++++++++++++++++ test/CallbackTest.php | 88 +++ test/Compress/Bz2Test.php | 175 ++++++ test/Compress/GzTest.php | 194 +++++++ test/Compress/LzfTest.php | 57 ++ test/Compress/RarTest.php | 304 ++++++++++ test/Compress/TarLoadArchiveTarTest.php | 36 ++ test/Compress/TarTest.php | 232 ++++++++ test/Compress/ZipTest.php | 287 ++++++++++ test/CompressTest.php | 234 ++++++++ test/DecompressTest.php | 100 ++++ test/DecryptTest.php | 136 +++++ test/DigitsTest.php | 88 +++ test/DirTest.php | 40 ++ test/Encrypt/BlockCipherTest.php | 221 +++++++ test/Encrypt/OpensslTest.php | 306 ++++++++++ test/EncryptTest.php | 141 +++++ test/File/DecryptTest.php | 122 ++++ test/File/EncryptTest.php | 118 ++++ test/File/LowerCaseTest.php | 123 ++++ test/File/RenameTest.php | 419 ++++++++++++++ test/File/UpperCaseTest.php | 123 ++++ test/FilterChainTest.php | 179 ++++++ test/HtmlEntitiesTest.php | 267 +++++++++ test/InflectorTest.php | 472 +++++++++++++++ test/IntTest.php | 44 ++ test/NullTest.php | 294 ++++++++++ test/PregReplaceTest.php | 100 ++++ test/RealPathTest.php | 109 ++++ test/StaticFilterTest.php | 116 ++++ test/StringToLowerTest.php | 161 ++++++ test/StringToUpperTest.php | 161 ++++++ test/StringTrimTest.php | 167 ++++++ test/StripNewlinesTest.php | 45 ++ test/StripTagsTest.php | 541 ++++++++++++++++++ test/Word/CamelCaseToDashTest.php | 34 ++ test/Word/CamelCaseToSeparatorTest.php | 44 ++ test/Word/CamelCaseToUnderscoreTest.php | 58 ++ test/Word/DashToCamelCaseTest.php | 34 ++ test/Word/DashToSeparatorTest.php | 45 ++ test/Word/DashToUnderscoreTest.php | 34 ++ test/Word/SeparatorToCamelCaseTest.php | 78 +++ test/Word/SeparatorToDashTest.php | 45 ++ test/Word/SeparatorToSeparatorTest.php | 55 ++ test/Word/UnderscoreToCamelCaseTest.php | 72 +++ test/Word/UnderscoreToDashTest.php | 34 ++ test/Word/UnderscoreToSeparatorTest.php | 45 ++ .../Compress/First/Second/zipextracted.txt | 1 + test/_files/Compress/First/zipextracted.txt | 1 + test/_files/Compress/zipextracted.txt | 1 + test/_files/TestNamespace/MyDigits.php | 24 + test/_files/TestNamespace/StringEquals.php | 60 ++ test/_files/TestNamespace/ValidatorBroker.php | 20 + test/_files/TestNamespace/ValidatorLoader.php | 26 + test/_files/_testDir2/.placeholder | 0 test/_files/compressed.rar | Bin 0 -> 483 bytes test/_files/encryption.txt | 1 + test/_files/evil.zip | Bin 0 -> 172 bytes test/_files/file.1 | 0 .../tests/Zend/Filter/_files/zipextracted.txt | 1 + test/_files/latin-1-dash-only.txt | 1 + test/_files/latin-1-text.txt | 1 + test/_files/privatekey.pem | 15 + test/_files/publickey.pem | 18 + test/_files/testfile.txt | 0 test/_files/testfile2.txt | 1 + test/_files/testkey.txt | 1 + test/bootstrap.php | 34 ++ 141 files changed, 14581 insertions(+) create mode 100644 .coveralls.yml create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .php_cs create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 phpunit.xml.travis create mode 100644 src/AbstractFilter.php create mode 100644 src/AbstractUnicode.php create mode 100644 src/BaseName.php create mode 100644 src/Boolean.php create mode 100644 src/Callback.php create mode 100644 src/Compress.php create mode 100644 src/Compress/AbstractCompressionAlgorithm.php create mode 100644 src/Compress/Bz2.php create mode 100644 src/Compress/CompressionAlgorithmInterface.php create mode 100644 src/Compress/Gz.php create mode 100644 src/Compress/Lzf.php create mode 100644 src/Compress/Rar.php create mode 100644 src/Compress/Tar.php create mode 100644 src/Compress/Zip.php create mode 100644 src/Decompress.php create mode 100644 src/Decrypt.php create mode 100644 src/Digits.php create mode 100644 src/Dir.php create mode 100644 src/Encrypt.php create mode 100644 src/Encrypt/BlockCipher.php create mode 100644 src/Encrypt/EncryptionAlgorithmInterface.php create mode 100644 src/Encrypt/Openssl.php create mode 100644 src/Exception/BadMethodCallException.php create mode 100644 src/Exception/DomainException.php create mode 100644 src/Exception/ExceptionInterface.php create mode 100644 src/Exception/ExtensionNotLoadedException.php create mode 100644 src/Exception/InvalidArgumentException.php create mode 100644 src/Exception/RuntimeException.php create mode 100644 src/File/Decrypt.php create mode 100644 src/File/Encrypt.php create mode 100644 src/File/LowerCase.php create mode 100644 src/File/Rename.php create mode 100644 src/File/UpperCase.php create mode 100644 src/FilterChain.php create mode 100644 src/FilterInterface.php create mode 100644 src/FilterPluginManager.php create mode 100644 src/HtmlEntities.php create mode 100644 src/Inflector.php create mode 100644 src/Int.php create mode 100644 src/Null.php create mode 100644 src/PregReplace.php create mode 100644 src/RealPath.php create mode 100644 src/StaticFilter.php create mode 100644 src/StringToLower.php create mode 100644 src/StringToUpper.php create mode 100644 src/StringTrim.php create mode 100644 src/StripNewlines.php create mode 100644 src/StripTags.php create mode 100644 src/Word/AbstractSeparator.php create mode 100644 src/Word/CamelCaseToDash.php create mode 100644 src/Word/CamelCaseToSeparator.php create mode 100644 src/Word/CamelCaseToUnderscore.php create mode 100644 src/Word/DashToCamelCase.php create mode 100644 src/Word/DashToSeparator.php create mode 100644 src/Word/DashToUnderscore.php create mode 100644 src/Word/SeparatorToCamelCase.php create mode 100644 src/Word/SeparatorToDash.php create mode 100644 src/Word/SeparatorToSeparator.php create mode 100644 src/Word/UnderscoreToCamelCase.php create mode 100644 src/Word/UnderscoreToDash.php create mode 100644 src/Word/UnderscoreToSeparator.php create mode 100644 test/BaseNameTest.php create mode 100644 test/BooleanTest.php create mode 100644 test/CallbackTest.php create mode 100644 test/Compress/Bz2Test.php create mode 100644 test/Compress/GzTest.php create mode 100644 test/Compress/LzfTest.php create mode 100644 test/Compress/RarTest.php create mode 100644 test/Compress/TarLoadArchiveTarTest.php create mode 100644 test/Compress/TarTest.php create mode 100644 test/Compress/ZipTest.php create mode 100644 test/CompressTest.php create mode 100644 test/DecompressTest.php create mode 100644 test/DecryptTest.php create mode 100644 test/DigitsTest.php create mode 100644 test/DirTest.php create mode 100644 test/Encrypt/BlockCipherTest.php create mode 100644 test/Encrypt/OpensslTest.php create mode 100644 test/EncryptTest.php create mode 100644 test/File/DecryptTest.php create mode 100644 test/File/EncryptTest.php create mode 100644 test/File/LowerCaseTest.php create mode 100644 test/File/RenameTest.php create mode 100644 test/File/UpperCaseTest.php create mode 100644 test/FilterChainTest.php create mode 100644 test/HtmlEntitiesTest.php create mode 100644 test/InflectorTest.php create mode 100644 test/IntTest.php create mode 100644 test/NullTest.php create mode 100644 test/PregReplaceTest.php create mode 100644 test/RealPathTest.php create mode 100644 test/StaticFilterTest.php create mode 100644 test/StringToLowerTest.php create mode 100644 test/StringToUpperTest.php create mode 100644 test/StringTrimTest.php create mode 100644 test/StripNewlinesTest.php create mode 100644 test/StripTagsTest.php create mode 100644 test/Word/CamelCaseToDashTest.php create mode 100644 test/Word/CamelCaseToSeparatorTest.php create mode 100644 test/Word/CamelCaseToUnderscoreTest.php create mode 100644 test/Word/DashToCamelCaseTest.php create mode 100644 test/Word/DashToSeparatorTest.php create mode 100644 test/Word/DashToUnderscoreTest.php create mode 100644 test/Word/SeparatorToCamelCaseTest.php create mode 100644 test/Word/SeparatorToDashTest.php create mode 100644 test/Word/SeparatorToSeparatorTest.php create mode 100644 test/Word/UnderscoreToCamelCaseTest.php create mode 100644 test/Word/UnderscoreToDashTest.php create mode 100644 test/Word/UnderscoreToSeparatorTest.php create mode 100644 test/_files/Compress/First/Second/zipextracted.txt create mode 100644 test/_files/Compress/First/zipextracted.txt create mode 100644 test/_files/Compress/zipextracted.txt create mode 100644 test/_files/TestNamespace/MyDigits.php create mode 100644 test/_files/TestNamespace/StringEquals.php create mode 100644 test/_files/TestNamespace/ValidatorBroker.php create mode 100644 test/_files/TestNamespace/ValidatorLoader.php create mode 100644 test/_files/_testDir2/.placeholder create mode 100644 test/_files/compressed.rar create mode 100644 test/_files/encryption.txt create mode 100644 test/_files/evil.zip create mode 100644 test/_files/file.1 create mode 100644 test/_files/home/sasa/code/zf2/tests/Zend/Filter/_files/zipextracted.txt create mode 100644 test/_files/latin-1-dash-only.txt create mode 100644 test/_files/latin-1-text.txt create mode 100644 test/_files/privatekey.pem create mode 100644 test/_files/publickey.pem create mode 100644 test/_files/testfile.txt create mode 100644 test/_files/testfile2.txt create mode 100644 test/_files/testkey.txt create mode 100644 test/bootstrap.php diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 00000000..53bda829 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,3 @@ +coverage_clover: clover.xml +json_path: coveralls-upload.json +src_dir: src diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..85dc9a8c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +/test export-ignore +/vendor export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +.php_cs export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4cac0a21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.buildpath +.DS_Store +.idea +.project +.settings/ +.*.sw* +.*.un~ +nbproject +tmp/ + +clover.xml +coveralls-upload.json +phpunit.xml +vendor diff --git a/.php_cs b/.php_cs new file mode 100644 index 00000000..bf4b799f --- /dev/null +++ b/.php_cs @@ -0,0 +1,43 @@ +notPath('TestAsset') + ->notPath('_files') + ->filter(function (SplFileInfo $file) { + if (strstr($file->getPath(), 'compatibility')) { + return false; + } + }); +$config = Symfony\CS\Config\Config::create(); +$config->level(null); +$config->fixers( + array( + 'braces', + 'duplicate_semicolon', + 'elseif', + 'empty_return', + 'encoding', + 'eof_ending', + 'function_call_space', + 'function_declaration', + 'indentation', + 'join_function', + 'line_after_namespace', + 'linefeed', + 'lowercase_keywords', + 'parenthesis', + 'multiple_use', + 'method_argument_space', + 'object_operator', + 'php_closing_tag', + 'psr0', + 'remove_lines_between_uses', + 'short_tag', + 'standardize_not_equal', + 'trailing_spaces', + 'unused_use', + 'visibility', + 'whitespacy_lines', + ) +); +$config->finder($finder); +return $config; diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..fe909ecb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,35 @@ +sudo: false + +language: php + +matrix: + fast_finish: true + include: + - php: 5.5 + - php: 5.6 + env: + - EXECUTE_TEST_COVERALLS=true + - EXECUTE_CS_CHECK=true + - php: 7 + - php: hhvm + allow_failures: + - php: 7 + - php: hhvm + +notifications: + irc: "irc.freenode.org#zftalk.dev" + email: false + +before_install: + - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi + +install: + - composer install --no-interaction --prefer-source + +script: + - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/phpunit -c phpunit.xml.travis --coverage-clover clover.xml ; fi + - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then ./vendor/bin/phpunit -c phpunit.xml.travis ; fi + - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/php-cs-fixer fix -v --diff --dry-run --config-file=.php_cs ; fi + +after_script: + - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/coveralls ; fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..c121e3ae --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,229 @@ +# CONTRIBUTING + +## RESOURCES + +If you wish to contribute to Zend Framework, please be sure to +read/subscribe to the following resources: + + - [Coding Standards](https://github.com/zendframework/zf2/wiki/Coding-Standards) + - [Contributor's Guide](http://framework.zend.com/participate/contributor-guide) + - ZF Contributor's mailing list: + Archives: http://zend-framework-community.634137.n4.nabble.com/ZF-Contributor-f680267.html + Subscribe: zf-contributors-subscribe@lists.zend.com + - ZF Contributor's IRC channel: + #zftalk.dev on Freenode.net + +If you are working on new features or refactoring [create a proposal](https://github.com/zendframework/zend-filter/issues/new). + +## Reporting Potential Security Issues + +If you have encountered a potential security vulnerability, please **DO NOT** report it on the public +issue tracker: send it to us at [zf-security@zend.com](mailto:zf-security@zend.com) instead. +We will work with you to verify the vulnerability and patch it as soon as possible. + +When reporting issues, please provide the following information: + +- Component(s) affected +- A description indicating how to reproduce the issue +- A summary of the security vulnerability and impact + +We request that you contact us via the email address above and give the project +contributors a chance to resolve the vulnerability and issue a new release prior +to any public exposure; this helps protect users and provides them with a chance +to upgrade and/or update in order to protect their applications. + +For sensitive email communications, please use [our PGP key](http://framework.zend.com/zf-security-pgp-key.asc). + +## RUNNING TESTS + +> ### Note: testing versions prior to 2.4 +> +> This component originates with Zend Framework 2. During the lifetime of ZF2, +> testing infrastructure migrated from PHPUnit 3 to PHPUnit 4. In most cases, no +> changes were necessary. However, due to the migration, tests may not run on +> versions < 2.4. As such, you may need to change the PHPUnit dependency if +> attempting a fix on such a version. + +To run tests: + +- Clone the repository: + + ```console + $ git clone git@github.com:zendframework/zend-filter.git + $ cd + ``` + +- Install dependencies via composer: + + ```console + $ curl -sS https://getcomposer.org/installer | php -- + $ ./composer.phar install + ``` + + If you don't have `curl` installed, you can also download `composer.phar` from https://getcomposer.org/ + +- Run the tests via `phpunit` and the provided PHPUnit config, like in this example: + + ```console + $ ./vendor/bin/phpunit + ``` + +You can turn on conditional tests with the phpunit.xml file. +To do so: + + - Copy `phpunit.xml.dist` file to `phpunit.xml` + - Edit `phpunit.xml` to enable any specific functionality you + want to test, as well as to provide test values to utilize. + +## Running Coding Standards Checks + +This component uses [php-cs-fixer](http://cs.sensiolabs.org/) for coding +standards checks, and provides configuration for our selected checks. +`php-cs-fixer` is installed by default via Composer. + +To run checks only: + +```console +$ ./vendor/bin/php-cs-fixer fix . -v --diff --dry-run --config-file=.php_cs +``` + +To have `php-cs-fixer` attempt to fix problems for you, omit the `--dry-run` +flag: + +```console +$ ./vendor/bin/php-cs-fixer fix . -v --diff --config-file=.php_cs +``` + +If you allow php-cs-fixer to fix CS issues, please re-run the tests to ensure +they pass, and make sure you add and commit the changes after verification. + +## Recommended Workflow for Contributions + +Your first step is to establish a public repository from which we can +pull your work into the master repository. We recommend using +[GitHub](https://github.com), as that is where the component is already hosted. + +1. Setup a [GitHub account](http://github.com/), if you haven't yet +2. Fork the repository (http://github.com/zendframework/zend-filter) +3. Clone the canonical repository locally and enter it. + + ```console + $ git clone git://github.com:zendframework/zend-filter.git + $ cd zend-filter + ``` + +4. Add a remote to your fork; substitute your GitHub username in the command + below. + + ```console + $ git remote add {username} git@github.com:{username}/zend-filter.git + $ git fetch {username} + ``` + +### Keeping Up-to-Date + +Periodically, you should update your fork or personal repository to +match the canonical ZF repository. Assuming you have setup your local repository +per the instructions above, you can do the following: + + +```console +$ git checkout master +$ git fetch origin +$ git rebase origin/master +# OPTIONALLY, to keep your remote up-to-date - +$ git push {username} master:master +``` + +If you're tracking other branches -- for example, the "develop" branch, where +new feature development occurs -- you'll want to do the same operations for that +branch; simply substitute "develop" for "master". + +### Working on a patch + +We recommend you do each new feature or bugfix in a new branch. This simplifies +the task of code review as well as the task of merging your changes into the +canonical repository. + +A typical workflow will then consist of the following: + +1. Create a new local branch based off either your master or develop branch. +2. Switch to your new local branch. (This step can be combined with the + previous step with the use of `git checkout -b`.) +3. Do some work, commit, repeat as necessary. +4. Push the local branch to your remote repository. +5. Send a pull request. + +The mechanics of this process are actually quite trivial. Below, we will +create a branch for fixing an issue in the tracker. + +```console +$ git checkout -b hotfix/9295 +Switched to a new branch 'hotfix/9295' +``` + +... do some work ... + + +```console +$ git commit +``` + +... write your log message ... + + +```console +$ git push {username} hotfix/9295:hotfix/9295 +Counting objects: 38, done. +Delta compression using up to 2 threads. +Compression objects: 100% (18/18), done. +Writing objects: 100% (20/20), 8.19KiB, done. +Total 20 (delta 12), reused 0 (delta 0) +To ssh://git@github.com/{username}/zend-filter.git + b5583aa..4f51698 HEAD -> master +``` + +To send a pull request, you have two options. + +If using GitHub, you can do the pull request from there. Navigate to +your repository, select the branch you just created, and then select the +"Pull Request" button in the upper right. Select the user/organization +"zendframework" as the recipient. + +If using your own repository - or even if using GitHub - you can use `git +format-patch` to create a patchset for us to apply; in fact, this is +**recommended** for security-related patches. If you use `format-patch`, please +send the patches as attachments to: + +- zf-devteam@zend.com for patches without security implications +- zf-security@zend.com for security patches + +#### What branch to issue the pull request against? + +Which branch should you issue a pull request against? + +- For fixes against the stable release, issue the pull request against the + "master" branch. +- For new features, or fixes that introduce new elements to the public API (such + as new public methods or properties), issue the pull request against the + "develop" branch. + +### Branch Cleanup + +As you might imagine, if you are a frequent contributor, you'll start to +get a ton of branches both locally and on your remote. + +Once you know that your changes have been accepted to the master +repository, we suggest doing some cleanup of these branches. + +- Local branch cleanup + + ```console + $ git branch -d + ``` + +- Remote branch removal + + ```console + $ git push {username} : + ``` diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..6eab5aa1 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,27 @@ +Copyright (c) 2005-2015, Zend Technologies USA, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of Zend Technologies USA, Inc. nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..7324e500 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# zend-filter + +The `Zend\Filter` component provides a set of commonly needed data filters. It +also provides a simple filter chaining mechanism by which multiple filters may +be applied to a single datum in a user-defined order. + + +- File issues at https://github.com/zendframework/zend-filter/issues +- Documentation is at http://framework.zend.com/manual/current/en/index.html#zend-filter diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..439e4103 --- /dev/null +++ b/composer.json @@ -0,0 +1,45 @@ +{ + "name": "zendframework/zend-filter", + "description": "provides a set of commonly needed data filters", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "filter" + ], + "homepage": "https://github.com/zendframework/zend-filter", + "autoload": { + "psr-4": { + "Zend\\Filter": "src/" + } + }, + "require": { + "php": ">=5.3.3", + "zendframework/zend-stdlib": "self.version" + }, + "require-dev": { + "zendframework/zend-crypt": "self.version", + "zendframework/zend-servicemanager": "self.version", + "zendframework/zend-uri": "self.version", + "fabpot/php-cs-fixer": "1.7.*", + "satooshi/php-coveralls": "dev-master", + "phpunit/PHPUnit": "~4.0" + }, + "suggest": { + "zendframework/zend-crypt": "Zend\\Crypt component", + "zendframework/zend-i18n": "Zend\\I18n component", + "zendframework/zend-servicemanager": "Zend\\ServiceManager component", + "zendframework/zend-uri": "Zend\\Uri component for UriNormalize filter", + "zendframework/zend-validator": "Zend\\Validator component" + }, + "extra": { + "branch-alias": { + "dev-master": "2.4-dev", + "dev-develop": "2.5-dev" + } + }, + "autoload-dev": { + "psr-4": { + "ZendTest\\Filter\\": "test/" + } + } +} \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..97777b3e --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,41 @@ + + + + + ./test/ + + + + + + disable + + + + + + ./src + + + + + + + + + + + + + + diff --git a/phpunit.xml.travis b/phpunit.xml.travis new file mode 100644 index 00000000..97777b3e --- /dev/null +++ b/phpunit.xml.travis @@ -0,0 +1,41 @@ + + + + + ./test/ + + + + + + disable + + + + + + ./src + + + + + + + + + + + + + + diff --git a/src/AbstractFilter.php b/src/AbstractFilter.php new file mode 100644 index 00000000..9aece481 --- /dev/null +++ b/src/AbstractFilter.php @@ -0,0 +1,117 @@ + $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/src/AbstractUnicode.php b/src/AbstractUnicode.php new file mode 100644 index 00000000..c535c7bb --- /dev/null +++ b/src/AbstractUnicode.php @@ -0,0 +1,64 @@ +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/src/BaseName.php b/src/BaseName.php new file mode 100644 index 00000000..84578d7b --- /dev/null +++ b/src/BaseName.php @@ -0,0 +1,31 @@ + '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 integer|array $type + * @throws Exception\InvalidArgumentException + * @return Boolean + */ + 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 boolean $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 Boolean + */ + public function setCasting($flag = true) + { + $this->options['casting'] = (boolean) $flag; + return $this; + } + + /** + * Returns the casting option + * + * @return boolean + */ + public function getCasting() + { + return $this->options['casting']; + } + + /** + * @param array|Traversable $translations + * @return Boolean + */ + 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/src/Callback.php b/src/Callback.php new file mode 100644 index 00000000..650850ea --- /dev/null +++ b/src/Callback.php @@ -0,0 +1,105 @@ + null, + 'callback_params' => array() + ); + + /** + * @param array|Traversable $options + */ + 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 + * @return Callback + */ + 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 mixed $params + * @return Callback + */ + public function setCallbackParams($params) + { + $this->options['callback_params'] = (array) $params; + return $this; + } + + /** + * Get parameters for the callback + * + * @return mixed + */ + 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/src/Compress.php b/src/Compress.php new file mode 100644 index 00000000..2de28e4b --- /dev/null +++ b/src/Compress.php @@ -0,0 +1,208 @@ +setAdapter($options); + } elseif ($options instanceof Compress\CompressionAlgorithmInterface) { + $this->setAdapter($options); + } elseif (is_array($options)) { + $this->setOptions($options); + } + } + + /** + * Set filter setate + * + * @param array $options + * @return Compress + */ + 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 + * + * @return string + * @throws Exception\InvalidArgumentException + */ + 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 Compress + * @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 Compress + */ + 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_Filter + * + * Compresses the content $value with the defined settings + * + * @param string $value Content to compress + * @return string The compressed content + */ + public function filter($value) + { + return $this->getAdapter()->compress($value); + } +} diff --git a/src/Compress/AbstractCompressionAlgorithm.php b/src/Compress/AbstractCompressionAlgorithm.php new file mode 100644 index 00000000..5bd98369 --- /dev/null +++ b/src/Compress/AbstractCompressionAlgorithm.php @@ -0,0 +1,81 @@ +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 AbstractCompressionAlgorithm + */ + 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/src/Compress/Bz2.php b/src/Compress/Bz2.php new file mode 100644 index 00000000..a53b6a32 --- /dev/null +++ b/src/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 + */ + 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 integer + */ + public function getBlocksize() + { + return $this->options['blocksize']; + } + + /** + * Sets a new blocksize + * + * @param integer $level + * @return Bz2 + */ + 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 Bz2 + */ + 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(); + if (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/src/Compress/CompressionAlgorithmInterface.php b/src/Compress/CompressionAlgorithmInterface.php new file mode 100644 index 00000000..44cbd0ca --- /dev/null +++ b/src/Compress/CompressionAlgorithmInterface.php @@ -0,0 +1,43 @@ + 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 + */ + 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 integer + */ + public function getLevel() + { + return $this->options['level']; + } + + /** + * Sets a new compression level + * + * @param integer $level + * @return Gz + */ + 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 Gz + * @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 Gz + */ + 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(); + if (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) { + throw new Exception\RuntimeException('Error during decompression'); + } + + return $compressed; + } + + /** + * Returns the adapter name + * + * @return string + */ + public function toString() + { + return 'Gz'; + } +} diff --git a/src/Compress/Lzf.php b/src/Compress/Lzf.php new file mode 100644 index 00000000..bc1edc1b --- /dev/null +++ b/src/Compress/Lzf.php @@ -0,0 +1,79 @@ + 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 + */ + 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 Rar + * @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 Rar + */ + 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 Rar + */ + 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 Rar + * @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 boolean + * @throws Exception\RuntimeException if archive not found, cannot be opened, + * or error during decompression + */ + public function decompress($content) + { + $archive = $this->getArchive(); + + 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/src/Compress/Tar.php b/src/Compress/Tar.php new file mode 100644 index 00000000..d5701ea2 --- /dev/null +++ b/src/Compress/Tar.php @@ -0,0 +1,236 @@ + 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 + */ + 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 Tar + */ + 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 Tar + * @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 Tar + * @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 = ucfirst(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/src/Compress/Zip.php b/src/Compress/Zip.php new file mode 100644 index 00000000..fe1bc2da --- /dev/null +++ b/src/Compress/Zip.php @@ -0,0 +1,317 @@ + 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 + */ + 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 Zip + */ + 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 + * @return Zip + */ + 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 = $this->getArchive(); + + if (empty($archive) || !file_exists($archive)) { + throw new Exception\RuntimeException('ZIP Archive not found'); + } + + $archive = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, realpath($content)); + $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/src/Decompress.php b/src/Decompress.php new file mode 100644 index 00000000..13129fc4 --- /dev/null +++ b/src/Decompress.php @@ -0,0 +1,33 @@ +getAdapter()->decompress($value); + } +} diff --git a/src/Decrypt.php b/src/Decrypt.php new file mode 100644 index 00000000..77693a3d --- /dev/null +++ b/src/Decrypt.php @@ -0,0 +1,33 @@ +adapter->decrypt($value); + } +} diff --git a/src/Digits.php b/src/Digits.php new file mode 100644 index 00000000..b7713822 --- /dev/null +++ b/src/Digits.php @@ -0,0 +1,42 @@ +setAdapter($options); + } + + /** + * Returns the name of the set adapter + * + * @return string + */ + public function getAdapter() + { + return $this->adapter->toString(); + } + + /** + * Sets new encryption options + * + * @param string|array $options (Optional) Encryption options + * @return Encrypt + * @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') and ($part != 'set')) or !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) + { + return $this->adapter->encrypt($value); + } +} diff --git a/src/Encrypt/BlockCipher.php b/src/Encrypt/BlockCipher.php new file mode 100644 index 00000000..8b10e791 --- /dev/null +++ b/src/Encrypt/BlockCipher.php @@ -0,0 +1,268 @@ + 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 + * 'iv' => initialization vector + * ) + */ + protected $encryption = array( + 'key' => 'ZendFramework', + 'key_iteration' => 5000, + 'algorithm' => 'aes', + 'hash' => 'sha256', + 'vector' => null, + ); + + /** + * 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 BlockCipher + * @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 BlockCipher + * @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; + } + + /** + * Returns the compression + * + * @return array + */ + public function getCompression() + { + return $this->compression; + } + + /** + * Sets a internal compression for values to encrypt + * + * @param string|array $compression + * @return BlockCipher + */ + 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 + * @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()); + } catch (SymmetricException\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/src/Encrypt/EncryptionAlgorithmInterface.php b/src/Encrypt/EncryptionAlgorithmInterface.php new file mode 100644 index 00000000..9e5f73ac --- /dev/null +++ b/src/Encrypt/EncryptionAlgorithmInterface.php @@ -0,0 +1,43 @@ + 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 boolean + */ + 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 Openssl + * @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 \Zend\Filter\Encrypt\Openssl + */ + 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 Openssl + */ + 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 $options Envelope keys + * @return \Zend\Filter\Encrypt\Openssl + */ + 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 Openssl + */ + public function setPassphrase($passphrase) + { + $this->passphrase = $passphrase; + return $this; + } + + /** + * Returns the compression + * + * @return array + */ + public function getCompression() + { + return $this->compression; + } + + /** + * Sets a internal compression for values to encrypt + * + * @param string|array $compression + * @return Openssl + */ + 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 boolean + */ + public function getPackage() + { + return $this->package; + } + + /** + * Sets if the envelope keys should be included in the encrypted value + * + * @param boolean $package + * @return Openssl + */ + public function setPackage($package) + { + $this->package = (boolean) $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 $key => $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/src/Exception/BadMethodCallException.php b/src/Exception/BadMethodCallException.php new file mode 100644 index 00000000..716386c1 --- /dev/null +++ b/src/Exception/BadMethodCallException.php @@ -0,0 +1,15 @@ +filename; + } + + /** + * Sets the new filename where the content will be stored + * + * @param string $filename (Optional) New filename to set + * @return Decrypt + */ + 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 $value Full path of file to change + * @return string The filename which has been set, or false when there were errors + * @throws Exception\InvalidArgumentException + * @throws Exception\RuntimeException + */ + public function filter($value) + { + 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}'"); + } + + return $this->filename; + } +} diff --git a/src/File/Encrypt.php b/src/File/Encrypt.php new file mode 100644 index 00000000..a7d5883b --- /dev/null +++ b/src/File/Encrypt.php @@ -0,0 +1,91 @@ +filename; + } + + /** + * Sets the new filename where the content will be stored + * + * @param string $filename (Optional) New filename to set + * @return Encrypt + */ + 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 $value Full path of file to change + * @return string The filename which has been set, or false when there were errors + * @throws Exception\InvalidArgumentException + * @throws Exception\RuntimeException + */ + public function filter($value) + { + 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}'"); + } + + return $this->filename; + } +} diff --git a/src/File/LowerCase.php b/src/File/LowerCase.php new file mode 100644 index 00000000..3148c9a7 --- /dev/null +++ b/src/File/LowerCase.php @@ -0,0 +1,56 @@ + 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 ? + * + * @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 ? + * + * @param string|array $options Old file or directory to be rewritten + * @return \Zend\Filter\File\Rename + */ + 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 ? + * + * @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 boolean $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'] == true) && (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 $value Full path of file to change + * @throws Exception\RuntimeException + * @return string The new filename which has been set, or false when there were errors + */ + public function filter($value) + { + $file = $this->getNewName($value, true); + if (is_string($file)) { + 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)); + } + + 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'] = (boolean) $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; + } + + $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; + } + } + + if (!isset($rename['source'])) { + return $file; + } + + if (!isset($rename['target']) or ($rename['target'] == '*')) { + $rename['target'] = $rename['source']; + } + + if (is_dir($rename['target'])) { + $name = basename($rename['source']); + $last = $rename['target'][strlen($rename['target']) - 1]; + if (($last != '/') and ($last != '\\')) { + $rename['target'] .= DIRECTORY_SEPARATOR; + } + + $rename['target'] .= $name; + } + + return $rename; + } +} diff --git a/src/File/UpperCase.php b/src/File/UpperCase.php new file mode 100644 index 00000000..05a42de2 --- /dev/null +++ b/src/File/UpperCase.php @@ -0,0 +1,56 @@ +filters = new PriorityQueue(); + + if (null !== $options) { + $this->setOptions($options); + } + } + + 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 FilterChain + */ + public function setPluginManager(FilterPluginManager $plugins) + { + $this->plugins = $plugins; + return $this; + } + + /** + * Retrieve a filter plugin by name + * + * @param mixed $name + * @param array $options + * @return Filter + */ + 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) + * @return FilterChain + */ + 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 broker, 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 FilterChain + */ + 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 FilterChain + */ + public function merge(FilterChain $filterChain) + { + foreach ($filterChain->filters as $filter) { + $this->attach($filter); + } + + 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; + } +} diff --git a/src/FilterInterface.php b/src/FilterInterface.php new file mode 100644 index 00000000..b246861a --- /dev/null +++ b/src/FilterInterface.php @@ -0,0 +1,27 @@ + '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', + 'compresstar' => 'Zend\Filter\Compress\Tar', + 'compresszip' => 'Zend\Filter\Compress\Zip', + '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', + 'fileuppercase' => 'Zend\Filter\File\UpperCase', + 'htmlentities' => 'Zend\Filter\HtmlEntities', + 'inflector' => 'Zend\Filter\Inflector', + 'int' => 'Zend\Filter\Int', + 'localizedtonormalized' => 'Zend\Filter\LocalizedToNormalized', + 'normalizedtolocalized' => 'Zend\Filter\NormalizedToLocalized', + 'null' => 'Zend\Filter\Null', + 'numberformat' => 'Zend\I18n\Filter\NumberFormat', + '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', + '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/src/HtmlEntities.php b/src/HtmlEntities.php new file mode 100644 index 00000000..809598ae --- /dev/null +++ b/src/HtmlEntities.php @@ -0,0 +1,201 @@ +setQuoteStyle($options['quotestyle']); + $this->setEncoding($options['encoding']); + $this->setDoubleQuote($options['doublequote']); + } + + /** + * Returns the quoteStyle option + * + * @return integer + */ + public function getQuoteStyle() + { + return $this->quoteStyle; + } + + /** + * Sets the quoteStyle option + * + * @param integer $quoteStyle + * @return HtmlEntities 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 HtmlEntities + */ + 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 HtmlEntities Provides a fluent interface + */ + public function setCharSet($charSet) + { + return $this->setEncoding($charSet); + } + + /** + * Returns the doubleQuote option + * + * @return boolean + */ + public function getDoubleQuote() + { + return $this->doubleQuote; + } + + /** + * Sets the doubleQuote option + * + * @param boolean $doubleQuote + * @return HtmlEntities Provides a fluent interface + */ + public function setDoubleQuote($doubleQuote) + { + $this->doubleQuote = (boolean) $doubleQuote; + return $this; + } + + /** + * Defined by Zend\Filter\FilterInterface + * + * Returns the string $value, converting characters to their corresponding HTML entity + * equivalents where they exist + * + * @param string $value + * @throws Exception\DomainException + * @return string + */ + public function filter($value) + { + $filtered = htmlentities((string) $value, $this->getQuoteStyle(), $this->getEncoding(), $this->getDoubleQuote()); + if (strlen((string) $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', (string) $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/src/Inflector.php b/src/Inflector.php new file mode 100644 index 00000000..1f6829c5 --- /dev/null +++ b/src/Inflector.php @@ -0,0 +1,475 @@ +setOptions($options); + } + + /** + * Retrieve plugin broker + * + * @return FilterPluginManager + */ + public function getPluginManager() + { + if (!$this->pluginManager instanceof FilterPluginManager) { + $this->setPluginManager(new FilterPluginManager()); + } + + return $this->pluginManager; + } + + /** + * Set plugin manager + * + * @param FilterPluginManager $manager + * @return Inflector + */ + public function setPluginManager(FilterPluginManager $manager) + { + $this->pluginManager = $manager; + return $this; + } + + /** + * Set options + * + * @param array|Traversable $options + * @return Inflector + */ + 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 $throwTargetExceptions + * @return Inflector + */ + 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 Inflector + */ + 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 + * @return Inflector + */ + public function setTarget($target) + { + $this->target = (string) $target; + return $this; + } + + /** + * Retrieve target + * + * @return string + */ + public function getTarget() + { + return $this->target; + } + + /** + * Set Target Reference + * + * @param reference $target + * @return Inflector + */ + public function setTargetReference(&$target) + { + $this->target =& $target; + return $this; + } + + /** + * SetRules() is the same as calling addRules() with the exception that it + * clears the rules before adding them. + * + * @param array $rules + * @return Inflector + */ + public function setRules(Array $rules) + { + $this->clearRules(); + $this->addRules($rules); + return $this; + } + + /** + * AddRules(): 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 + * @return Inflector + */ + 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; + } + + /** + * getRule() 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; + } + + /** + * ClearRules() clears the rules currently in the inflector + * + * @return Inflector + */ + 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 Inflector + */ + 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 Inflector + */ + 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 Inflector + */ + 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 Inflector + */ + public function setStaticRuleReference($name, &$reference) + { + $name = $this->_normalizeSpec($name); + $this->rules[$name] =& $reference; + return $this; + } + + /** + * Inflect + * + * @param string|array $source + * @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/src/Int.php b/src/Int.php new file mode 100644 index 00000000..80bfd534 --- /dev/null +++ b/src/Int.php @@ -0,0 +1,31 @@ + '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 $options 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 integer|array $type + * @throws Exception\InvalidArgumentException + * @return Boolean + */ + 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/src/PregReplace.php b/src/PregReplace.php new file mode 100644 index 00000000..54e35ddb --- /dev/null +++ b/src/PregReplace.php @@ -0,0 +1,136 @@ + 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 PregReplace + * @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)) + )); + } + $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 PregReplace + * @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 ($this->options['pattern'] === null) { + throw new Exception\RuntimeException(sprintf( + 'Filter %s does not have a valid pattern set', + get_called_class() + )); + } + + return preg_replace($this->options['pattern'], $this->options['replacement'], $value); + } +} diff --git a/src/RealPath.php b/src/RealPath.php new file mode 100644 index 00000000..4dc78bd9 --- /dev/null +++ b/src/RealPath.php @@ -0,0 +1,120 @@ + true + ); + + /** + * Class constructor + * + * @param boolean|\Traversable $options 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 boolean $flag Path must exist + * @return RealPath + */ + public function setExists($flag = true) + { + $this->options['exists'] = (boolean) $flag; + return $this; + } + + /** + * Returns true if the filtered path must exist + * + * @return boolean + */ + public function getExists() + { + return $this->options['exists']; + } + + /** + * Defined by Zend\Filter\FilterInterface + * + * Returns realpath($value) + * + * @param string $value + * @return string + */ + public function filter($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/src/StaticFilter.php b/src/StaticFilter.php new file mode 100644 index 00000000..24305c45 --- /dev/null +++ b/src/StaticFilter.php @@ -0,0 +1,75 @@ +setShareByDefault(false); + } + self::$plugins = $manager; + } + + /** + * Get plugin manager for loading filter classes + * + * @return FilterPluginManager + */ + public static function getPluginManager() + { + if (null === self::$plugins) { + static::setPluginManager(new FilterPluginManager()); + } + return self::$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/src/StringToLower.php b/src/StringToLower.php new file mode 100644 index 00000000..32d2feb2 --- /dev/null +++ b/src/StringToLower.php @@ -0,0 +1,60 @@ + null, + ); + + /** + * Constructor + * + * @param string|array|Traversable $options 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 + * + * @param string $value + * @return string + */ + public function filter($value) + { + if ($this->options['encoding'] !== null) { + return mb_strtolower((string) $value, $this->options['encoding']); + } + + return strtolower((string) $value); + } +} diff --git a/src/StringToUpper.php b/src/StringToUpper.php new file mode 100644 index 00000000..e830be58 --- /dev/null +++ b/src/StringToUpper.php @@ -0,0 +1,60 @@ + null, + ); + + /** + * Constructor + * + * @param string|array|Traversable $options 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 + * + * @param string $value + * @return string + */ + public function filter($value) + { + if ($this->options['encoding'] !== null) { + return mb_strtoupper((string) $value, $this->options['encoding']); + } + + return strtoupper((string) $value); + } +} diff --git a/src/StringTrim.php b/src/StringTrim.php new file mode 100644 index 00000000..0dbfb7bb --- /dev/null +++ b/src/StringTrim.php @@ -0,0 +1,114 @@ + null, + ); + + /** + * Sets filter options + * + * @param string|array|Traversable $options + */ + 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 StringTrim Provides a fluent interface + */ + public function setCharList($charList) + { + if (empty($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) + { + // Do not filter non-string values + if (!is_string($value)) { + return $value; + } + + if (null === $this->options['charlist']) { + return $this->unicodeTrim((string) $value); + } + + return $this->unicodeTrim((string) $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/src/StripNewlines.php b/src/StripNewlines.php new file mode 100644 index 00000000..1a62872d --- /dev/null +++ b/src/StripNewlines.php @@ -0,0 +1,32 @@ + 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 StripTags 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 StripTags 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 + * + * @todo improve docblock descriptions + * + * @param string $value + * @return string + */ + public function filter($value) + { + $value = (string) $value; + + // Strip HTML comments first + while (strpos($value, ''; + $expected = ''; + $this->assertEquals($expected, $filter($input)); + } + + /** + * Ensures that a comment wrapped with other strings is stripped + * + * @return void + */ + public function testFilterCommentWrapped() + { + $filter = $this->_filter; + $input = 'foobar'; + $expected = 'foobar'; + $this->assertEquals($expected, $filter($input)); + } + + /** + * Ensures that a closing angle bracket in an allowed attribute does not break the parser + * + * @return void + * @link http://framework.zend.com/issues/browse/ZF-3278 + */ + public function testClosingAngleBracketInAllowedAttributeValue() + { + $filter = $this->_filter; + $tagsAllowed = array( + 'a' => 'href' + ); + $filter->setTagsAllowed($tagsAllowed); + $input = ''; + $expected = ''; + $this->assertEquals($expected, $filter($input)); + } + + /** + * Ensures that an allowed attribute's value may end with an equals sign '=' + * + * @group ZF-3293 + * @group ZF-5983 + */ + public function testAllowedAttributeValueMayEndWithEquals() + { + $filter = $this->_filter; + $tagsAllowed = array( + 'element' => 'attribute' + ); + $filter->setTagsAllowed($tagsAllowed); + $input = 'contents'; + $this->assertEquals($input, $filter($input)); + } + + /** + * @group ZF-5983 + */ + public function testDisallowedAttributesSplitOverMultipleLinesShouldBeStripped() + { + $filter = $this->_filter; + $tagsAllowed = array('a' => 'href'); + $filter->setTagsAllowed($tagsAllowed); + $input = 'http://framework.zend.com/issues'; + $filtered = $filter($input); + $this->assertNotContains('onclick', $filtered); + } + + /** + * @ZF-8828 + */ + public function testFilterIsoChars() + { + $filter = $this->_filter; + $input = 'äöüäöü'; + $expected = 'äöüäöü'; + $this->assertEquals($expected, $filter($input)); + + $input = 'äöüäöü'; + $input = iconv("UTF-8", "ISO-8859-1", $input); + $output = $filter($input); + $this->assertFalse(empty($output)); + } + + /** + * @ZF-8828 + */ + public function testFilterIsoCharsInComment() + { + $filter = $this->_filter; + $input = 'äöüäöü'; + $expected = 'äöüäöü'; + $this->assertEquals($expected, $filter($input)); + + $input = 'äöüäöü'; + $input = iconv("UTF-8", "ISO-8859-1", $input); + $output = $filter($input); + $this->assertFalse(empty($output)); + } + + /** + * @ZF-8828 + */ + public function testFilterSplitCommentTags() + { + $filter = $this->_filter; + $input = 'äöüüßüßüß<-->äöü'; + $expected = 'äöüäöü'; + $this->assertEquals($expected, $filter($input)); + } + + /** + * @group ZF-9434 + */ + public function testCommentWithTagInSameLine() + { + $filter = $this->_filter; + $input = 'test test
div-content
'; + $expected = 'test test div-content'; + $this->assertEquals($expected, $filter($input)); + } + + /** + * @group ZF-9833 + */ + public function testMultiParamArray() + { + $filter = new StripTagsFilter(array("a","b","hr"),array(),true); + + $input = 'test test
div-content
'; + $expected = 'test
test div-content'; + $this->assertEquals($expected, $filter->filter($input)); + } + + /** + * @group ZF-9828 + */ + public function testMultiQuoteInput() + { + $filter = new StripTagsFilter( + array( + 'allowTags' => 'img', + 'allowAttribs' => array('width', 'height', 'src') + ) + ); + + $input = ''; + $expected = ''; + $this->assertEquals($expected, $filter->filter($input)); + } + + /** + * @group ZF-10256 + */ + public function testNotClosedHtmlCommentAtEndOfString() + { + $input = 'text