diff --git a/.editorconfig b/.editorconfig index a84f86c..de2fe7e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,13 +6,11 @@ root = true [*] indent_style = space indent_size = 4 +charset = "utf-8" end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true -[*.bat] -end_of_line = crlf - [*.yml] indent_style = space -indent_size = 2 \ No newline at end of file +indent_size = 2 diff --git a/.gitattributes b/.gitattributes index 17493ec..d912dbb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,8 +1,11 @@ # Remove files for archives generated using `git archive` +CONTRIBUTING.md export-ignore .editorconfig export-ignore .gitattributes export-ignore .gitignore export-ignore -.semver export-ignore phpunit.xml.dist export-ignore .travis.yml export-ignore +.scrutinizer.yml export-ignore tests export-ignore +docs export-ignore +.github export-ignore diff --git a/.gitignore b/.gitignore index 364f9bd..5209c91 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ -/composer.lock -/plugins -/vendor +*.pyc +docs/_build +phpunit.xml +vendor/ +composer.lock +tmp +.phpunit.result.cache diff --git a/.semver b/.semver deleted file mode 100644 index 3581f63..0000000 --- a/.semver +++ /dev/null @@ -1,6 +0,0 @@ ---- -:major: 0 -:minor: 0 -:patch: 0 -:special: '' -:metadata: '' diff --git a/.sticker.yml b/.sticker.yml new file mode 100644 index 0000000..08993b0 --- /dev/null +++ b/.sticker.yml @@ -0,0 +1,11 @@ +linters: + phpcs: + standard: CakePHP4 + extensions: 'php' + fixer: true +files: + ignore: + - 'vendor/*' +fixers: + enable: true + workflow: commit diff --git a/.travis.yml b/.travis.yml index 9c729f2..af10731 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,52 +1,65 @@ language: php php: - - 5.6 - 7.2 - 7.3 - -services: - - mysql - - postgresql + - 7.4 env: matrix: - - DB=sqlite db_dsn='sqlite:///:memory:' - - DB=mysql db_dsn='mysql://root@127.0.0.1/cakephp_test?init[]=SET sql_mode = "STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"' + - DB=mysql db_dsn='mysql://root@127.0.0.1/cakephp_test' - DB=pgsql db_dsn='postgres://postgres@127.0.0.1/cakephp_test' + - DB=sqlite db_dsn='sqlite:///:memory:' global: - DEFAULT=1 +services: + - mysql + - postgresql + matrix: fast_finish: true include: - - php: 7.3 + - php: 7.2 env: PHPCS=1 DEFAULT=0 - - php: 5.6 - env: PREFER_LOWEST=1 + - php: 7.2 + env: STATIC_ANALYSIS=1 DEFAULT=0 before_script: - if [[ $TRAVIS_PHP_VERSION != 7.2 ]]; then phpenv config-rm xdebug.ini; fi - - if [[ $PREFER_LOWEST != 1 ]]; then composer install --no-interaction; fi + - if [[ $PREFER_LOWEST != 1 ]]; then composer update --no-interaction; fi - if [[ $PREFER_LOWEST == 1 ]]; then composer update --no-interaction --prefer-lowest --prefer-stable; fi - - if [[ $DB == 'mysql' ]]; then mysql -e 'CREATE DATABASE cakephp_test;'; fi - - if [[ $DB == 'pgsql' ]]; then psql -c 'CREATE DATABASE cakephp_test;' -U postgres; fi + - if [[ $DB = 'mysql' ]]; then mysql -u root -e 'CREATE DATABASE cakephp_test;'; fi + - if [[ $DB = 'pgsql' ]]; then psql -c 'CREATE DATABASE cakephp_test;' -U postgres; fi - - if [[ $PHPCS == '1' ]]; then composer require cakephp/cakephp-codesniffer:^3.0; fi + - if [[ $PHPCS = 1 ]]; then composer require cakephp/cakephp-codesniffer:^4.0; fi + - if [[ $STATIC_ANALYSIS = 1 ]]; then composer require --dev phpstan/phpstan:^0.12 psalm/phar:^3.7; fi script: - - if [[ $DEFAULT == 1 && $TRAVIS_PHP_VERSION == 7.2 ]]; then vendor/bin/phpunit --coverage-clover=clover.xml; fi - - if [[ $DEFAULT == 1 && $TRAVIS_PHP_VERSION != 7.2 ]]; then vendor/bin/phpunit; fi + - | + if [[ $DEFAULT = 1 && $TRAVIS_PHP_VERSION = 7.2 ]]; then + mkdir -p build/logs + vendor/bin/phpunit --coverage-clover=build/logs/clover.xml + fi + + - if [[ $DEFAULT = 1 && $TRAVIS_PHP_VERSION != 7.2 ]]; then vendor/bin/phpunit; fi + + - if [[ $PHPCS = 1 ]]; then vendor/bin/phpcs -p --standard=vendor/cakephp/cakephp-codesniffer/CakePHP ./src ./tests; fi - - if [[ $PHPCS == 1 ]]; then vendor/bin/phpcs -p --extensions=php --standard=vendor/cakephp/cakephp-codesniffer/CakePHP ./src ./tests; fi + - if [[ $STATIC_ANALYSIS = 1 ]]; then vendor/bin/phpstan.phar analyse src && vendor/bin/psalm.phar --show-info=false; fi after_success: - - if [[ $DEFAULT == 1 && $TRAVIS_PHP_VERSION == 7.2 ]]; then bash <(curl -s https://codecov.io/bash); fi + - | + if [[ $DEFAULT = 1 && $TRAVIS_PHP_VERSION = 7.2 ]]; then + wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.1.0/php-coveralls.phar + chmod +x php-coveralls.phar + ./php-coveralls.phar + fi notifications: email: false diff --git a/README.md b/README.md index bc73b1d..f965c16 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,12 @@ Load the plugin by either running this console command: bin/cake plugin load Muffin/Obfuscate ``` +or by manually adding the following line to `src/Application.php`: + +```php +$this->addPlugin('Muffin/Obfuscate'); +``` + Lastly, composer install (any combination of) the obfuscation libraries you want to use in your application: diff --git a/VERSION b/VERSION deleted file mode 100644 index ae39fab..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -v0.0.0 diff --git a/composer.json b/composer.json index 5b9dd03..3656285 100644 --- a/composer.json +++ b/composer.json @@ -31,11 +31,14 @@ "source": "https://github.com/usemuffin/obfuscate" }, "require": { - "cakephp/orm": "^3.5" + "cakephp/orm": "^4.0" }, "require-dev": { - "cakephp/cakephp": "^3.5", - "phpunit/phpunit": "^5.7.14|^6.0", + "cakephp/cakephp": "^4.0", + "cakephp/cakephp-codesniffer": "^4.0", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "~8.5.0", + "psalm/phar": "^3.9", "zackkitzmiller/tiny": "^1.2", "jenssegers/optimus": "^0.2", "hashids/hashids": "^1.0.5" diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..f51e71c --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,2 @@ +parameters: + ignoreErrors: [] diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..ea3d1a8 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,7 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 6 + checkGenericClassInNonGenericObjectType: false + checkMissingIterableValueType: false diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 813017e..56c0f03 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -3,6 +3,7 @@ bootstrap="./tests/bootstrap.php" colors="true" stopOnFailure="false" + convertDeprecationsToExceptions="false" > diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..15a7cd6 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Model/Behavior/ObfuscateBehavior.php b/src/Model/Behavior/ObfuscateBehavior.php index dde0a92..bc4ff04 100644 --- a/src/Model/Behavior/ObfuscateBehavior.php +++ b/src/Model/Behavior/ObfuscateBehavior.php @@ -1,14 +1,17 @@ verifyConfig(); } @@ -48,10 +50,10 @@ public function initialize(array $config) * * @return void */ - public function verifyConfig() + public function verifyConfig(): void { $strategy = $this->getConfig('strategy'); - if (!$strategy) { + if (empty($strategy)) { throw new Exception('Missing required obfuscation strategy.'); } @@ -67,14 +69,17 @@ public function verifyConfig() /** * Callback to obfuscate the record(s)' primary key returned after a save operation. * - * @param \Cake\ORM\Behavior\Event $event Event. - * @param \Cake\ORM\Behavior\EntityInterface $entity Entity. + * @param \Cake\Event\EventInterface $event EventInterface. + * @param \Cake\Datasource\EntityInterface $entity Entity. * @param \ArrayObject $options Options. * @return void */ - public function afterSave(Event $event, EntityInterface $entity, ArrayObject $options) + public function afterSave(EventInterface $event, EntityInterface $entity, ArrayObject $options) { $pk = $this->_table->getPrimaryKey(); + if (is_array($pk)) { + throw new RuntimeException('Composite primary keys are not supported.'); + } $entity->set($pk, $this->obfuscate($entity->{$pk})); $entity->setDirty($pk, false); } @@ -82,13 +87,13 @@ public function afterSave(Event $event, EntityInterface $entity, ArrayObject $op /** * Callback to set the `obfuscated` finder on all associations. * - * @param \Cake\ORM\Behavior\Event $event Event. + * @param \Cake\Event\EventInterface $event EventInterface. * @param \Cake\ORM\Query $query Query. * @param \ArrayObject $options Options. * @param bool $primary True if this is the primary table. * @return void */ - public function beforeFind(Event $event, Query $query, ArrayObject $options, $primary) + public function beforeFind(EventInterface $event, Query $query, ArrayObject $options, $primary) { if (empty($options['obfuscate']) || !$primary) { return; @@ -96,8 +101,12 @@ public function beforeFind(Event $event, Query $query, ArrayObject $options, $pr $query->traverseExpressions(function ($expression) { $pk = $this->_table->getPrimaryKey(); + if (is_array($pk)) { + throw new RuntimeException('Composite primary keys are not supported.'); + } + if ( - method_exists($expression, 'getField') + $expression instanceof Comparison && in_array($expression->getField(), [$pk, $this->_table->aliasField($pk)]) ) { $expression->setValue($this->elucidate($expression->getValue())); @@ -173,7 +182,7 @@ public function elucidate($str) /** * Get the configured strategy. * - * @return \Muffin\Obfuscate\Model\Behavior\ObfuscateStrategy\StrategyInterface + * @return \Muffin\Obfuscate\Model\Behavior\Strategy\StrategyInterface */ public function strategy() { diff --git a/src/Model/Behavior/Strategy/HashidStrategy.php b/src/Model/Behavior/Strategy/HashidStrategy.php index af9980c..145c5a5 100644 --- a/src/Model/Behavior/Strategy/HashidStrategy.php +++ b/src/Model/Behavior/Strategy/HashidStrategy.php @@ -1,4 +1,6 @@ _salt = $salt; - $this->_minLength = $minLength; - $this->_alphabet = $alphabet; if ($alphabet === null) { $this->_hashid = new Hashids($salt, $minLength); @@ -65,12 +46,12 @@ public function __construct($salt = null, $minLength = 0, $alphabet = null) /** * {@inheritdoc} * - * @param string $str String to obfuscate. + * @param int|string $str String to obfuscate. * @return string */ public function obfuscate($str) { - return $this->_hashid->encode($str); + return $this->_hashid->encode((string)$str); } /** diff --git a/src/Model/Behavior/Strategy/OptimusStrategy.php b/src/Model/Behavior/Strategy/OptimusStrategy.php index 9258455..595eb9e 100644 --- a/src/Model/Behavior/Strategy/OptimusStrategy.php +++ b/src/Model/Behavior/Strategy/OptimusStrategy.php @@ -1,10 +1,18 @@ _optimus->encode($str); + if (!is_numeric($str)) { + throw new InvalidArgumentException('Argument should be an integer'); + } + + return (string)$this->_optimus->encode((int)$str); } /** * {@inheritdoc} * * @param string $str String to elucidate. - * @return int + * @return string */ public function elucidate($str) { - return $this->_optimus->decode($str); + if (!is_numeric($str)) { + throw new InvalidArgumentException('Argument should be an integer'); + } + + return (string)$this->_optimus->decode((int)$str); } } diff --git a/src/Model/Behavior/Strategy/StrategyInterface.php b/src/Model/Behavior/Strategy/StrategyInterface.php index 431d1ba..c5770b3 100644 --- a/src/Model/Behavior/Strategy/StrategyInterface.php +++ b/src/Model/Behavior/Strategy/StrategyInterface.php @@ -1,4 +1,6 @@ _tiny->to($str); + return $this->_tiny->to((string)$str); } /** diff --git a/src/Plugin.php b/src/Plugin.php new file mode 100644 index 0000000..04692cd --- /dev/null +++ b/src/Plugin.php @@ -0,0 +1,19 @@ + 2, 'title' => 'Second Article'], ]; - public function init() + public function init(): void { $created = $modified = date('Y-m-d H:i:s'); array_walk($this->records, function (&$record) use ($created, $modified) { diff --git a/tests/Fixture/ArticlesTagsFixture.php b/tests/Fixture/ArticlesTagsFixture.php index 2bbe140..62b94d2 100644 --- a/tests/Fixture/ArticlesTagsFixture.php +++ b/tests/Fixture/ArticlesTagsFixture.php @@ -1,4 +1,6 @@ 'Jane'], ]; - public function init() + public function init(): void { $created = $modified = date('Y-m-d H:i:s'); array_walk($this->records, function (&$record) use ($created, $modified) { diff --git a/tests/Fixture/CommentsFixture.php b/tests/Fixture/CommentsFixture.php index 8c3842b..2c35c94 100644 --- a/tests/Fixture/CommentsFixture.php +++ b/tests/Fixture/CommentsFixture.php @@ -1,4 +1,6 @@ 1, 'title' => 'Hello Universe'], ]; - public function init() + public function init(): void { $created = $modified = date('Y-m-d H:i:s'); array_walk($this->records, function (&$record) use ($created, $modified) { diff --git a/tests/Fixture/TagsFixture.php b/tests/Fixture/TagsFixture.php index 69ac6e8..d4f5a25 100644 --- a/tests/Fixture/TagsFixture.php +++ b/tests/Fixture/TagsFixture.php @@ -1,4 +1,6 @@ 'Bar'], ]; - public function init() + public function init(): void { $created = $modified = date('Y-m-d H:i:s'); array_walk($this->records, function (&$record) use ($created, $modified) { diff --git a/tests/TestCase/Model/Behavior/ObfuscateBehaviorTest.php b/tests/TestCase/Model/Behavior/ObfuscateBehaviorTest.php index 758b7e4..7fd7e1b 100644 --- a/tests/TestCase/Model/Behavior/ObfuscateBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/ObfuscateBehaviorTest.php @@ -1,6 +1,9 @@ TableRegistry::get('Muffin/Obfuscate.ArticlesTags', ['table' => 'obfuscate_articles_tags']), ]); - $this->Obfuscate = $this->Articles->behaviors()->Obfuscate; + $this->Obfuscate = $this->Articles->getBehavior('Obfuscate'); } - public function tearDown() + public function tearDown(): void { parent::tearDown(); TableRegistry::clear(); } - /** - * @expectedException \Cake\Core\Exception\Exception - */ - public function testVerifyConfig() + public function testVerifyConfig(): void { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Missing required obfuscation strategy'); + $this->Articles->removeBehavior('Obfuscate'); $this->Articles->addBehavior('Muffin/Obfuscate.Obfuscate'); } - public function testAfterSave() + public function testAfterSave(): void { $entity = new Entity(['id' => 5, 'title' => 'foo']); $this->Articles->save($entity); @@ -83,7 +98,7 @@ public function testAfterSave() * Make sure primary keys in returned result set are obfuscated when using * the `obfuscate` custom finder. */ - public function testFindObfuscate() + public function testFindObfuscate(): void { $result = $this->Articles->find('obfuscate')->contain([ $this->Authors->getAlias(), @@ -101,7 +116,7 @@ public function testFindObfuscate() * Make sure primary keys in the returned result set are NOT obfuscated * when using default find. */ - public function testFindWithoutObfuscate() + public function testFindWithoutObfuscate(): void { $result = $this->Articles->find()->contain([ $this->Authors->getAlias(), @@ -119,7 +134,7 @@ public function testFindWithoutObfuscate() * Make sure we can search for records using obfuscated primary key when * using the `obfuscated` custom finder. */ - public function testFindObfuscated() + public function testFindObfuscated(): void { $results = $this->Articles->find('obfuscated') ->where(['id' => 'S']) @@ -131,7 +146,7 @@ public function testFindObfuscated() * Make sure we can search for records using non-obfuscated primary key * when using default find. */ - public function testFindWithoutObfuscated() + public function testFindWithoutObfuscated(): void { $results = $this->Articles->find() ->where(['id' => '1']) @@ -139,13 +154,13 @@ public function testFindWithoutObfuscated() $this->assertEquals('1', $results[0]['id']); } - public function testObfuscate() + public function testObfuscate(): void { - $this->assertEquals('S', $this->Articles->obfuscate(1)); + $this->assertEquals('S', $this->Articles->behaviors()->call('obfuscate', [1])); } - public function testElucidate() + public function testElucidate(): void { - $this->assertEquals(1, $this->Articles->elucidate('S')); + $this->assertEquals(1, $this->Articles->behaviors()->call('elucidate', ['S'])); } } diff --git a/tests/TestCase/Model/Behavior/Strategy/HashidStrategyTest.php b/tests/TestCase/Model/Behavior/Strategy/HashidStrategyTest.php index 0b2dd7f..8c2b8c1 100644 --- a/tests/TestCase/Model/Behavior/Strategy/HashidStrategyTest.php +++ b/tests/TestCase/Model/Behavior/Strategy/HashidStrategyTest.php @@ -1,46 +1,52 @@ strategy = new HashidStrategy('5SX0TEjkR1mLOw8Gvq2VyJxIFhgCAYidrclDWaM3so9bfzZpuUenKtP74QNH6B'); } - public function testObfuscate() + public function testObfuscate(): void { - $result = $this->assertEquals('k8', $this->strategy->obfuscate(1)); + $this->assertEquals('k8', $this->strategy->obfuscate(1)); } - public function testElucidate() + public function testElucidate(): void { $this->assertEquals(1, $this->strategy->elucidate('k8')); } - public function testMinLength() + public function testMinLength(): void { $this->strategy = new HashidStrategy('5SX0TEjkR1mLOw8Gvq2VyJxIFhgCAYidrclDWaM3so9bfzZpuUenKtP74QNH6B', 10); $this->assertEquals('qxPAk8pnOV', $this->strategy->obfuscate(1)); } - public function testCustomAlphabet() + public function testCustomAlphabet(): void { $this->strategy = new HashidStrategy('5SX0TEjkR1mLOw8Gvq2VyJxIFhgCAYidrclDWaM3so9bfzZpuUenKtP74QNH6B', 0, 'abcdefghijklmnopqrstuvwxyz'); $this->assertEquals('vg', $this->strategy->obfuscate(1)); } - /** - * @expectedException Exception - * @expectedExceptionMessage Missing salt for Hashid strategy - */ - public function testSaltException() + public function testSaltException(): void { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Missing salt for Hashid strategy'); new HashidStrategy(); } } diff --git a/tests/TestCase/Model/Behavior/Strategy/OptimusStrategyTest.php b/tests/TestCase/Model/Behavior/Strategy/OptimusStrategyTest.php index aba98ec..6e7ff21 100644 --- a/tests/TestCase/Model/Behavior/Strategy/OptimusStrategyTest.php +++ b/tests/TestCase/Model/Behavior/Strategy/OptimusStrategyTest.php @@ -1,23 +1,30 @@ strategy = new OptimusStrategy(2123809381, 1885413229, 146808189); } - public function testObfuscate() + public function testObfuscate(): void { $this->assertEquals(1985404696, $this->strategy->obfuscate(1)); } - public function testElucidate() + public function testElucidate(): void { - $this->assertEquals(1, $this->strategy->elucidate(1985404696)); + $this->assertEquals(1, $this->strategy->elucidate('1985404696')); } } diff --git a/tests/TestCase/Model/Behavior/Strategy/TinyStrategyTest.php b/tests/TestCase/Model/Behavior/Strategy/TinyStrategyTest.php index d539db8..11c4dd7 100644 --- a/tests/TestCase/Model/Behavior/Strategy/TinyStrategyTest.php +++ b/tests/TestCase/Model/Behavior/Strategy/TinyStrategyTest.php @@ -1,22 +1,29 @@ strategy = new TinyStrategy('5SX0TEjkR1mLOw8Gvq2VyJxIFhgCAYidrclDWaM3so9bfzZpuUenKtP74QNH6B'); } - public function testObfuscate() + public function testObfuscate(): void { $this->assertEquals('S', $this->strategy->obfuscate(1)); } - public function testElucidate() + public function testElucidate(): void { $this->assertEquals(1, $this->strategy->elucidate('S')); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index c6dc570..39f2a49 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,28 +1,90 @@ 'Muffin\Obfuscate\Test\App', + 'encoding' => 'UTF-8', + 'fullBaseUrl' => 'http://localhost' +]); +Cake\Core\Configure::write('debug', true); + +$TMP = new \Cake\Filesystem\Folder(TMP); +$TMP->create(TMP . 'cache/models', 0777); +$TMP->create(TMP . 'cache/persistent', 0777); +$TMP->create(TMP . 'cache/views', 0777); + +$cache = [ + 'default' => [ + 'engine' => 'File' + ], + '_cake_core_' => [ + 'className' => 'File', + 'prefix' => 'muffin_obfuscate_myapp_cake_core_', + 'path' => CACHE . 'persistent/', + 'serialize' => true, + 'duration' => '+10 seconds' + ], + '_cake_model_' => [ + 'className' => 'File', + 'prefix' => 'muffin_obfuscate_my_app_cake_model_', + 'path' => CACHE . 'models/', + 'serialize' => 'File', + 'duration' => '+10 seconds' + ] +]; + +Cake\Cache\Cache::setConfig($cache); +Cake\Core\Configure::write('Session', [ + 'defaults' => 'php' +]); + +// Ensure default test connection is defined +if (!getenv('db_dsn')) { + putenv('db_dsn=sqlite:///:memory:'); } -require $root . '/vendor/cakephp/cakephp/tests/bootstrap.php'; +Cake\Datasource\ConnectionManager::setConfig('test', [ + 'url' => getenv('db_dsn'), + 'timezone' => 'UTC' +]); + +Plugin::getCollection()->add(new \Muffin\Obfuscate\Plugin());