├── LICENSE ├── README.md ├── composer.json ├── config ├── Migrations │ └── 20200430170235_MigrationToolsTokens.php ├── app.default.php └── bootstrap.php ├── docs ├── Authentication │ └── LoginLink.md ├── Backend.md ├── Behavior │ ├── AfterSave.md │ ├── Bitmasked.md │ ├── Encryption.md │ ├── Jsonable.md │ ├── Passwordable.md │ ├── Reset.md │ ├── Slugged.md │ ├── String.md │ ├── Toggle.md │ └── Typographic.md ├── Command │ └── Inflect.md ├── Component │ ├── Common.md │ ├── Mobile.md │ └── RefererRedirect.md ├── Contributing.md ├── Controller │ └── Controller.md ├── Entity │ ├── Enum.md │ └── StaticEnum.md ├── Error │ └── ExceptionTrap.md ├── Helper │ ├── Common.md │ ├── Form.md │ ├── Format.md │ ├── Html.md │ ├── Icon.md │ ├── Meter.md │ ├── Progress.md │ ├── Tree.md │ └── Typography.md ├── I18n │ ├── DateTime.md │ └── I18n.md ├── Install.md ├── Mailer │ └── Email.md ├── Model │ ├── Table.md │ └── Tokens.md ├── README.md ├── Shims.md ├── Upgrade.md ├── Url │ └── Url.md ├── Utility │ └── FileLog.md └── Widget │ └── Datalist.md ├── phpcs.xml ├── phpstan.neon ├── resources └── locales │ ├── README.txt │ ├── cs │ └── tools.po │ ├── de │ └── tools.po │ ├── en │ └── tools.po │ ├── es │ └── tools.po │ └── tools.pot ├── src ├── Auth │ ├── AbstractPasswordHasher.php │ ├── DefaultPasswordHasher.php │ └── PasswordHasherFactory.php ├── Authenticator │ └── LoginLinkAuthenticator.php ├── Command │ └── InflectCommand.php ├── Controller │ ├── Admin │ │ ├── HelperController.php │ │ ├── PagesController.php │ │ └── ToolsController.php │ ├── Component │ │ ├── CommonComponent.php │ │ ├── MobileComponent.php │ │ ├── RefererRedirectComponent.php │ │ └── UrlComponent.php │ ├── Controller.php │ └── ShuntRequestController.php ├── Error │ ├── ErrorHandlerTrait.php │ ├── ErrorLogger.php │ ├── ExceptionTrap.php │ └── Middleware │ │ └── ErrorHandlerMiddleware.php ├── Form │ └── ContactForm.php ├── I18n │ ├── Date.php │ ├── DateTime.php │ ├── DateTimeHelper.php │ └── Number.php ├── Identifier │ └── LoginLinkIdentifier.php ├── Mailer │ ├── Mailer.php │ ├── MailerAwareTrait.php │ └── Message.php ├── Model │ ├── Behavior │ │ ├── AfterSaveBehavior.php │ │ ├── BitmaskedBehavior.php │ │ ├── ConfirmableBehavior.php │ │ ├── EncryptionBehavior.php │ │ ├── JsonableBehavior.php │ │ ├── NeighborBehavior.php │ │ ├── PasswordableBehavior.php │ │ ├── ResetBehavior.php │ │ ├── SluggedBehavior.php │ │ ├── StringBehavior.php │ │ ├── ToggleBehavior.php │ │ ├── TypeMapBehavior.php │ │ └── TypographicBehavior.php │ ├── Entity │ │ ├── Entity.php │ │ ├── EnumTrait.php │ │ └── Token.php │ ├── Enum │ │ └── EnumOptionsTrait.php │ └── Table │ │ ├── Table.php │ │ └── TokensTable.php ├── ToolsPlugin.php ├── Utility │ ├── Clock.php │ ├── DateTime.php │ ├── FileLog.php │ ├── Language.php │ ├── Mime.php │ ├── MimeTypes.php │ ├── Number.php │ ├── Random.php │ ├── Text.php │ └── Utility.php └── View │ ├── Helper │ ├── CommonHelper.php │ ├── FormHelper.php │ ├── FormatHelper.php │ ├── GravatarHelper.php │ ├── HtmlHelper.php │ ├── HtmlTrait.php │ ├── IconHelper.php │ ├── MeterHelper.php │ ├── NumberHelper.php │ ├── ObfuscateHelper.php │ ├── ProgressHelper.php │ ├── QrCodeHelper.php │ ├── TextHelper.php │ ├── TimeHelper.php │ ├── TimelineHelper.php │ ├── TreeHelper.php │ ├── TypographyHelper.php │ ├── UrlHelper.php │ └── UrlTrait.php │ ├── Icon │ ├── AbstractIcon.php │ ├── BootstrapIcon.php │ ├── Collector │ │ ├── BootstrapIconCollector.php │ │ ├── FeatherIconCollector.php │ │ ├── FontAwesome4IconCollector.php │ │ ├── FontAwesome5IconCollector.php │ │ ├── FontAwesome6IconCollector.php │ │ └── MaterialIconCollector.php │ ├── FeatherIcon.php │ ├── FontAwesome4Icon.php │ ├── FontAwesome5Icon.php │ ├── FontAwesome6Icon.php │ ├── IconCollection.php │ ├── IconInterface.php │ └── MaterialIcon.php │ └── Widget │ └── DatalistWidget.php ├── templates ├── Admin │ ├── Helper │ │ ├── bitmasks.php │ │ └── chars.php │ ├── Pages │ │ └── index.php │ └── Tools │ │ └── index.php └── element │ └── pagination.php └── tests ├── Fixture ├── AfterTreesFixture.php ├── ArticlesFixture.php ├── AuthorsFixture.php ├── BitmaskedCommentsFixture.php ├── DataFixture.php ├── JsonableCommentsFixture.php ├── MultiColumnUsersFixture.php ├── PostsFixture.php ├── ResetCommentsFixture.php ├── RolesFixture.php ├── SessionsFixture.php ├── SluggedArticlesFixture.php ├── StoriesFixture.php ├── StringCommentsFixture.php ├── ToggleAddressesFixture.php ├── TokensFixture.php └── ToolsUsersFixture.php ├── TestCase ├── Authenticator │ └── LoginLinkAuthenticatorTest.php ├── BootstrapTest.php ├── Command │ └── InflectCommandTest.php ├── Controller │ ├── Admin │ │ ├── HelperControllerTest.php │ │ ├── PagesControllerTest.php │ │ └── ToolsControllerTest.php │ ├── Component │ │ ├── CommonComponentTest.php │ │ ├── MobileComponentTest.php │ │ ├── RefererRedirectComponentTest.php │ │ └── UrlComponentTest.php │ ├── ControllerTest.php │ └── ShuntRequestControllerTest.php ├── ErrorHandler │ ├── ErrorLoggerTest.php │ └── ExceptionTrapTest.php ├── Form │ └── ContactFormTest.php ├── HtmlDom │ └── HtmlDomTest.php ├── I18n │ ├── DateTest.php │ ├── DateTimeHelperTest.php │ ├── DateTimeTest.php │ └── NumberTest.php ├── Identifier │ └── LoginLinkIdentifierTest.php ├── Mailer │ ├── EmailTest.php │ ├── MailerTest.php │ └── MessageTest.php ├── Model │ ├── Behavior │ │ ├── AfterSaveBehaviorTest.php │ │ ├── BitmaskedBehaviorTest.php │ │ ├── ConfirmableBehaviorTest.php │ │ ├── EncryptionBehaviorTest.php │ │ ├── JsonableBehaviorTest.php │ │ ├── NeighborBehaviorTest.php │ │ ├── PasswordableBehaviorTest.php │ │ ├── ResetBehaviorTest.php │ │ ├── SluggedBehaviorTest.php │ │ ├── StringBehaviorTest.php │ │ ├── ToggleBehaviorTest.php │ │ ├── TypeMapBehaviorTest.php │ │ └── TypographicBehaviorTest.php │ ├── Entity │ │ └── EntityTest.php │ ├── Enum │ │ └── EnumOptionsTraitTest.php │ └── Table │ │ ├── TableTest.php │ │ └── TokensTableTest.php ├── Utility │ ├── ClockTest.php │ ├── FileLogTest.php │ ├── LanguageTest.php │ ├── MimeTest.php │ ├── MimeTypesTest.php │ ├── RandomTest.php │ ├── TextTest.php │ └── UtilityTest.php └── View │ ├── Helper │ ├── CommonHelperTest.php │ ├── FormHelperTest.php │ ├── FormatHelperTest.php │ ├── GravatarHelperTest.php │ ├── HtmlHelperTest.php │ ├── IconHelperTest.php │ ├── MeterHelperTest.php │ ├── NumberHelperTest.php │ ├── ObfuscateHelperTest.php │ ├── ProgressHelperTest.php │ ├── QrCodeHelperTest.php │ ├── TextHelperTest.php │ ├── TimeHelperTest.php │ ├── TimelineHelperTest.php │ ├── TreeHelperTest.php │ ├── TypographyHelperTest.php │ └── UrlHelperTest.php │ ├── Icon │ ├── BootstrapIconTest.php │ ├── Collector │ │ ├── BootstrapIconCollectorTest.php │ │ ├── FeatherIconCollectorTest.php │ │ ├── FontAwesome4CollectorTest.php │ │ ├── FontAwesome5CollectorTest.php │ │ ├── FontAwesome6CollectorTest.php │ │ └── MaterialIconCollectorTest.php │ ├── FeatherIconTest.php │ ├── FontAwesome4IconTest.php │ ├── FontAwesome5IconTest.php │ ├── FontAwesome6IconTest.php │ ├── IconCollectionTest.php │ └── MaterialIconTest.php │ └── Widget │ └── DatalistWidgetTest.php ├── bootstrap.php ├── config ├── bootstrap.php └── routes.php ├── schema.php └── templates ├── email ├── html │ └── welcome.php └── text │ └── welcome.php └── layout ├── default.php └── email ├── html └── fancy.php └── text └── fancy.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Mark Scherer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CakePHP Tools Plugin 2 | [![CI](https://github.com/dereuromark/cakephp-tools/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/dereuromark/cakephp-tools/actions/workflows/ci.yml?query=branch%3Amaster) 3 | [![Coverage Status](https://img.shields.io/codecov/c/github/dereuromark/cakephp-tools/master.svg)](https://codecov.io/gh/dereuromark/cakephp-tools) 4 | [![Latest Stable Version](https://poser.pugx.org/dereuromark/cakephp-tools/v/stable.svg)](https://packagist.org/packages/dereuromark/cakephp-tools) 5 | [![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%208.1-8892BF.svg)](https://php.net/) 6 | [![License](https://poser.pugx.org/dereuromark/cakephp-tools/license.svg)](LICENSE) 7 | [![Total Downloads](https://poser.pugx.org/dereuromark/cakephp-tools/d/total.svg)](https://packagist.org/packages/dereuromark/cakephp-tools) 8 | [![Coding Standards](https://img.shields.io/badge/cs-PSR--2--R-yellow.svg)](https://github.com/php-fig-rectified/fig-rectified-standards) 9 | 10 | A CakePHP plugin containing several useful tools that can be used in many projects. 11 | 12 | ## Version notice 13 | 14 | This master branch only works for **CakePHP 5.1+**. See [version map](https://github.com/dereuromark/cakephp-tools/wiki#cakephp-version-map) for details. 15 | 16 | ## What is this plugin for? 17 | 18 | ### Enhancing the core 19 | - Auto-trim on POST (to make - especially notEmpty/notBlank - validation working properly). 20 | - Disable cache also works for older IE versions. 21 | - Provide enum support as "static enums" 22 | - Default settings for Paginator, ... can be set using Configure. 23 | - Provided a less error-prone inArray() method via Utility class and other usefulness. 24 | - TestSuite enhancements 25 | - A few more Database Type classes 26 | 27 | ### Additional features 28 | - Passwordable behavior allows easy to use password functionality for frontend and backend. 29 | - Slugged, Reset and other behaviors 30 | - Tree helper for working with (complex) trees and their output. 31 | - Progress and Meter helper for progress bar and meter bar elements (HTML5 and textual). 32 | - Text, Time, Number libs and helpers etc provide extended functionality if desired. 33 | - Gravatar and other useful small helpers 34 | - Timeline, Typography, etc provide additional helper functionality. 35 | - Email as a wrapper for core's Email adding some more usefulness and making debugging/testing easier. 36 | - I18n language detection and switching 37 | 38 | ### Providing 4.x shims 39 | This plugin for CakePHP 5 also contains some 4.x shims to ease migration of existing applications from 4.x to 5.x: 40 | - See [Shim](https://github.com/dereuromark/cakephp-shim) plugin for details on most of the provided shims. 41 | 42 | ## Installation & Docs 43 | 44 | - [Documentation](docs/README.md) 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dereuromark/cakephp-tools", 3 | "description": "A CakePHP plugin containing lots of useful and reusable tools", 4 | "license": "MIT", 5 | "type": "cakephp-plugin", 6 | "keywords": [ 7 | "cakephp", 8 | "plugin", 9 | "tools", 10 | "utils", 11 | "helpers", 12 | "components", 13 | "behaviors", 14 | "datasources" 15 | ], 16 | "authors": [ 17 | { 18 | "name": "Mark Scherer", 19 | "homepage": "https://www.dereuromark.de", 20 | "role": "Author" 21 | } 22 | ], 23 | "homepage": "https://github.com/dereuromark/cakephp-tools", 24 | "support": { 25 | "issues": "https://github.com/dereuromark/cakephp-tools/issues", 26 | "source": "https://github.com/dereuromark/cakephp-tools" 27 | }, 28 | "require": { 29 | "php": ">=8.1", 30 | "cakephp/cakephp": "^5.1.1", 31 | "dereuromark/cakephp-shim": "^3.0.0" 32 | }, 33 | "require-dev": { 34 | "fig-r/psr2r-sniffer": "dev-master", 35 | "mobiledetect/mobiledetectlib": "^4.8.09", 36 | "phpunit/phpunit": "^10.5 || ^11.5 || ^12.1", 37 | "cakephp/authentication": "^3.1.0", 38 | "yangqi/htmldom": "^1.0" 39 | }, 40 | "suggest": { 41 | "yangqi/htmldom": "For HtmlDom usage" 42 | }, 43 | "minimum-stability": "stable", 44 | "prefer-stable": true, 45 | "autoload": { 46 | "psr-4": { 47 | "Tools\\": "src/", 48 | "Tools\\Test\\Fixture\\": "tests/Fixture/" 49 | } 50 | }, 51 | "autoload-dev": { 52 | "psr-4": { 53 | "Cake\\PHPStan\\": "vendor/cakephp/cakephp/tests/PHPStan/", 54 | "Cake\\Test\\": "vendor/cakephp/cakephp/tests/", 55 | "TestApp\\": "tests/test_app/", 56 | "Tools\\Test\\": "tests/" 57 | } 58 | }, 59 | "config": { 60 | "allow-plugins": { 61 | "dealerdirect/phpcodesniffer-composer-installer": true 62 | }, 63 | "process-timeout": 600 64 | }, 65 | "scripts": { 66 | "cs-check": "phpcs --extensions=php", 67 | "cs-fix": "phpcbf --extensions=php", 68 | "lowest": "validate-prefer-lowest", 69 | "lowest-setup": "composer update --prefer-lowest --prefer-stable --prefer-dist --no-interaction && cp composer.json composer.backup && composer require --dev dereuromark/composer-prefer-lowest && mv composer.backup composer.json", 70 | "stan": "phpstan analyse", 71 | "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:^2.0.0 && mv composer.backup composer.json", 72 | "test": "phpunit", 73 | "test-coverage": "phpunit --log-junit webroot/coverage/unitreport.xml --coverage-html webroot/coverage --coverage-clover webroot/coverage/coverage.xml" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /config/Migrations/20200430170235_MigrationToolsTokens.php: -------------------------------------------------------------------------------- 1 | table('tokens')->exists()) { 17 | $this->table('tokens') 18 | ->renameColumn('key', 'token_key') 19 | ->update(); 20 | 21 | return; 22 | } 23 | 24 | $this->table('tokens') 25 | ->addColumn('user_id', 'integer', [ 26 | 'limit' => null, 27 | 'null' => true, 28 | ]) 29 | ->addColumn('type', 'string', [ 30 | 'comment' => 'e.g.:activate,reactivate', 31 | 'default' => null, 32 | 'limit' => 20, 33 | 'null' => false, 34 | ]) 35 | ->addColumn('token_key', 'string', [ 36 | 'default' => null, 37 | 'limit' => 60, 38 | 'null' => false, 39 | ]) 40 | ->addColumn('content', 'string', [ 41 | 'comment' => 'can transport some information', 42 | 'default' => null, 43 | 'limit' => 255, 44 | 'null' => true, 45 | ]) 46 | ->addColumn('used', 'integer', [ 47 | 'default' => 0, 48 | 'limit' => null, 49 | 'null' => false, 50 | ]) 51 | ->addColumn('created', 'datetime', [ 52 | 'default' => null, 53 | 'limit' => null, 54 | 'null' => true, 55 | ]) 56 | ->addColumn('modified', 'datetime', [ 57 | 'default' => null, 58 | 'limit' => null, 59 | 'null' => true, 60 | ]) 61 | ->addIndex(['user_id']) 62 | ->addIndex(['token_key'], ['unique' => true]) 63 | ->create(); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /config/app.default.php: -------------------------------------------------------------------------------- 1 | [ 5 | ], 6 | 7 | // Error handling around 404s 8 | 'Log' => [ 9 | 'debug' => [ 10 | 'scopes' => null, 11 | ], 12 | 'error' => [ 13 | 'scopes' => null, 14 | ], 15 | '404' => [ 16 | 'file' => '404', 17 | 'levels' => ['error'], 18 | 'scopes' => ['404'], 19 | ], 20 | ], 21 | 22 | // Controller pagination 23 | 'DataPreparation' => [ 24 | 'noTrim' => false, 25 | ], 26 | 27 | // Behaviors 28 | 'Passwordable' => [ 29 | ], 30 | 'Reset' => [ 31 | ], 32 | 'Slugged' => [ 33 | ], 34 | 35 | // Email 36 | 'Config' => [ 37 | 'systemEmail' => '', 38 | 'systemName' => '', 39 | 'adminEmail' => '', 40 | 'adminName' => '', 41 | 'xMailer' => '', 42 | 'live' => false, 43 | ], 44 | 45 | // Helpers 46 | 'Format' => [ 47 | 'templates' => [], 48 | ], 49 | 'Google' => [ 50 | ], 51 | 'Icon' => [ 52 | 'checkExistence' => false, 53 | 'sets' => [], 54 | 'map' => [], 55 | ], 56 | ]; 57 | -------------------------------------------------------------------------------- /docs/Backend.md: -------------------------------------------------------------------------------- 1 | # Backend 2 | 3 | See `/admin/tools` in your browser once you made sure the plugin is loaded with routes enabled. 4 | 5 | ## Available pages 6 | A list of all available pages of your "PagesController". 7 | Useful clickable quick-overview using the correct routing (dashed vs underscore etc). 8 | 9 | ## String analyzer 10 | This is a simple tool to analyze strings and find/explain possible (hidden) UTF8 characters. 11 | 12 | ## Bitmask updating 13 | When working with bitmasks this can help to update the records in the DB. 14 | -------------------------------------------------------------------------------- /docs/Behavior/AfterSave.md: -------------------------------------------------------------------------------- 1 | # AfterSave Behavior 2 | 3 | A CakePHP behavior that allows the entity to be available after the save in the state 4 | it was before the save. 5 | It also allows it to be available outside of the afterSave() callback later on, where needed. 6 | 7 | ## Introduction 8 | It takes a clone of the entity from beforeSave(). This allows all the 9 | info on it to be available in the afterSave() callback or from the outside without resetting (dirty, ...). 10 | 11 | This can be useful if one wants to compare what fields got changed, or e.g. for logging the diff. 12 | 13 | ### Technical limitation 14 | Make sure you do not further modify the entity in the table's beforeSave() then. As this would 15 | not be part of the cloned and stored entity here. 16 | 17 | ## Usage 18 | Attach it to your model's `Table` class in its `initialize()` method like so: 19 | ```php 20 | $this->addBehavior('Tools.AfterSave', $options); 21 | ``` 22 | 23 | Then inside your table you can do: 24 | ```php 25 | public function afterSave(Event $event, EntityInterface $entity, ArrayObject $options): void { 26 | $entityBefore = $this->getEntityBeforeSave(); 27 | // Now you can check diff dirty fields etc 28 | } 29 | ``` 30 | 31 | The same call could also be made from the calling layer/object on the table: 32 | ```php 33 | $table->saveOrFail(); 34 | $entityBefore = $table->getEntityBeforeSave(); 35 | ``` 36 | 37 | If you are using save(), make sure you check the result and that the save was successful. 38 | Only call this method after a successful save operation. 39 | Otherwise, there will not be an entity stored and you would get an exception here. 40 | 41 | Also note that by default the "dirty" fields should be present in the afterSave() callback still. 42 | Same for "original" values. 43 | So if your needs are bound to that callback only, this behavior might not be needed usually. 44 | -------------------------------------------------------------------------------- /docs/Behavior/Encryption.md: -------------------------------------------------------------------------------- 1 | # Encryption Behavior 2 | 3 | A CakePHP behavior to automatically encrypt and decrypt data passed through the ORM. 4 | 5 | ## Technical limitation 6 | * Be aware, that your table columns need to be in a **binary** format and **large enough** to contain the encrypted payload. Something like `varbinary(1024)` 7 | * You are no longer able to search, filter or join with those specific columns on a database level. 8 | * The encryption key needs to be at least 32 characters long. See [here](https://book.cakephp.org/5/en/core-libraries/security.html) to learn more. 9 | 10 | ## Usage 11 | Attach it to your model's `Table` class in its `initialize()` method like so: 12 | ```php 13 | $this->addBehavior('Tools.Encryption', [ 14 | 'fields' => ['secret_field'], 15 | 'key' => \Cake\Core\Configure::read('Security.encryption') 16 | ]); 17 | ``` 18 | 19 | After attaching the behavior, a call like 20 | 21 | ```php 22 | $user = $this->Users->newEmptyEntity(); 23 | $user = $this->Users->patchEntity($user, [ 24 | 'username' => 'cake', 25 | 'password' => 'a random generated string hopefully' 26 | 'secret_field' => 'my super mysterious secret' 27 | ]); 28 | $this->Users->saveOrFail($user); 29 | ``` 30 | 31 | will result in the `secret_field` to be automatically encrypted. 32 | 33 | Same goes for when you are fetching the entry from the ORM via 34 | 35 | ```php 36 | $user = $this->Users->get($id); 37 | // or 38 | $users = $this->Users->find()->all(); 39 | ``` 40 | 41 | will automatically decrypt the binary data. 42 | 43 | ## Recommendations 44 | 45 | * Please do not use encryption if you don't need it! Password authentication for user login should always be implemented via hashing, not encryption. 46 | * It is recommended to use a separate encryption key compared to your `Security.salt` value. 47 | -------------------------------------------------------------------------------- /docs/Behavior/String.md: -------------------------------------------------------------------------------- 1 | # String Behavior 2 | 3 | A CakePHP behavior to apply basic string operations for your input. 4 | 5 | Note that most string modification should be done once, on save. 6 | Prevent using output modification if possible as it is done on every fetch. 7 | 8 | ### Usage 9 | 10 | #### Input formatting 11 | Include behavior in your Table class as 12 | ```php 13 | $this->addBehavior('Tools.String', [ 14 | 'fields' => ['title'], 15 | 'input' => ['ucfirst'], 16 | ]); 17 | ``` 18 | This will `ucfirst()` the title prior to saving. 19 | 20 | Tip: If you have other behaviors that might modify the array data prior to saving, better use a lower (higher value) priority: 21 | ```php 22 | $this->addBehavior('Tools.String', [ 23 | ... 24 | 'priority' => 11, 25 | ]); 26 | ``` 27 | 28 | The input filters are an array and therefore can also be stacked. They will be executed in the order given. 29 | If string that function is expected to exist. You can also use callables and anonymous functions, of course. 30 | 31 | If you need to process different ones per field, use the following way to configure: 32 | ```php 33 | $this->addBehavior('Tools.String', [ 34 | 'fields' => [ 35 | 'title' => [ 36 | function(string $e): string { 37 | return ucwords($e); 38 | }, function(string $e): string { 39 | return str_replace(' ', '', $e); 40 | }, 41 | ], 42 | ], 43 | ]); 44 | ``` 45 | 46 | #### Output formatting 47 | Instead of the preferred input formatting you can also modify the output (for each find): 48 | ```php 49 | $this->addBehavior('Tools.String', [ 50 | ... 51 | 'output' => ['ucfirst'], 52 | ]); 53 | ``` 54 | 55 | 56 | ### Configuration 57 | 58 | - `clean`: true/false to also clean the input strings on beforeMarshal() and prio to validation. 59 | - `fields`: array of fields to apply the input/output filters to. 60 | - `input`: array of input filters to apply to the fields. 61 | - `output`: array of output filters to apply to the fields. 62 | 63 | When using `input` and "per field" input filters, the `fields` config is ignored. 64 | 65 | ### Examples 66 | 67 | Imagine the following config: 68 | ```php 69 | 'fields' => ['title', 'comment'], 70 | 'input' => ['strtolower', 'ucwords'], 71 | ``` 72 | 73 | And the input: 74 | ```php 75 | $data = [ 76 | 'title' => 'some nAme', 77 | 'comment' => 'myBlog', 78 | 'url' => 'www.dereuromark.de', 79 | ]; 80 | $comment = $this->Comments->newEntity($data); 81 | $result = $this->Comments->save($comment); 82 | ``` 83 | 84 | The title would be saved as `Some Name` and the comment as `MyBlog`. 85 | -------------------------------------------------------------------------------- /docs/Behavior/Toggle.md: -------------------------------------------------------------------------------- 1 | # Toggle Behavior 2 | 3 | A CakePHP behavior to handle unique toggles. 4 | 5 | An implementation of a unique field toggle per table or scope. 6 | This will ensure that on a set of records only one can be a "toggled" one, setting the others to false then. 7 | On delete it will give the toggle status to another record if applicable. 8 | 9 | ### Usage 10 | 11 | #### Basic usage 12 | Include behavior in your Table class as 13 | ```php 14 | $this->addBehavior('Tools.Toggle', [ 15 | 'field' => 'primary', 16 | 'scopeFields' => ['user_id'], 17 | 'scope' => [], 18 | ]); 19 | ``` 20 | 21 | Set the `field` to your table field. Optionally, you can also set scope fields and a manual scope. 22 | Those will always be taken into consideration as scope conditions. 23 | 24 | ### Configuration 25 | 26 | The `'findOrder'` can be used to further customize the find result when looking for a new toggle record. 27 | By default it will try to use the `modified` field as `DESC`. But maybe you would want it to sort by another field. 28 | -------------------------------------------------------------------------------- /docs/Behavior/Typographic.md: -------------------------------------------------------------------------------- 1 | # Typographic Behavior 2 | 3 | A CakePHP behavior to handle typographic consistency. 4 | 5 | The basic idea is to normalize all input into a standard typography (utf8 default). 6 | So different quotes like `»` or `“` end up as `"` in the database. 7 | Upon output one can the decide to re-apply localization here. 8 | 9 | See the [TypographyHelper](/docs/Helper/Typography.md) docs for output modification. 10 | 11 | ### Usage 12 | 13 | #### Basic usage 14 | Include behavior in your Table class as 15 | ```php 16 | $this->addBehavior('Tools.Typographic', [ 17 | 'fields' => ['content'], 18 | 'mergeQuotes' => false, 19 | ]); 20 | ``` 21 | 22 | Set the `fields` to your table fields you want to normalize. 23 | 24 | ### Configuration 25 | 26 | With `mergeQuotes` option you can define if both `"` and `'` should be merged into one of them. 27 | Defaults to `false` as they might be used nested for default input. 28 | -------------------------------------------------------------------------------- /docs/Command/Inflect.md: -------------------------------------------------------------------------------- 1 | # Inflect Command 2 | 3 | Display what the inflector would do internally. 4 | ``` 5 | bin/cake inflect {word} {action(s)} 6 | ``` 7 | 8 | "all" will display all actions and their results: 9 | ``` 10 | bin/cake inflect FooBar all 11 | ``` 12 | 13 | You can also just run specific actions or explore a chain of actions: 14 | 15 | ``` 16 | bin/cake inflect "Foo Bar" pluralize,underscore 17 | ``` 18 | should output something like 19 | ``` 20 | Foo Bar 21 | Chained: 22 | - Pluralized form : Foo Bars 23 | - under_scored_form : foo bars 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/Component/Common.md: -------------------------------------------------------------------------------- 1 | # Common component 2 | 3 | ## Trimming payload data 4 | By default, adding the Common component to your AppController will make sure your POST and query params are trimmed. 5 | This is needed to make - not only notEmpty - validation working properly. 6 | 7 | You can skip for certain actions using `'DataPreparation.notrim'` config key per use case. 8 | 9 | ## Is Post Check 10 | A convenience method can quickly check on a form being posted: 11 | ```php 12 | if ($this->Common->isPosted()) {} 13 | ``` 14 | Saves you the trouble of checking for `post`, `patch`, `put` etc together, and in most cases this is not necessary. It is only important it wasn't a `get` request. 15 | 16 | ## Secure redirects back 17 | Sometimes you want to post to an edit or delete form and make sure you get redirected back to the correct action including query strings (e.g. for filtering). 18 | Then you can pass `redirect` key as either as part of POST payload or as query string. 19 | 20 | ``` 21 | // In your action 22 | $redirectUrl = $this->Common->getSafeRedirectUrl(['action' => 'default']); 23 | return $this->redirect($redirectUrl); 24 | ``` 25 | 26 | It is important to not use the payload data without sanitation for security reasons (redirect forgery to external websites). 27 | 28 | ## Default URL params 29 | 30 | `CommonComponent::defaultUrlParams()` will give you the default params you might want to combine with your URL 31 | in order to always generate the right URLs even if inside plugins or prefixes. 32 | 33 | ## Current URL 34 | 35 | `$this->Common->currentUrl()` returns current url (with all missing params automatically added). 36 | 37 | ## autoRedirect() 38 | A shortcut convenience wrapper for referrer redirecting with fallback: 39 | ```php 40 | return $this->Common->autoRedirect($defaultUrl); 41 | ``` 42 | Set the 2nd param to true to allow redirecting to itself (if that was the referer). 43 | 44 | ## completeRedirect() 45 | Automatically also adds the query string into the redirect. Useful when you want to keep the filters and pass them around. 46 | ```php 47 | return $this->Common->completeRedirect($redirectUrl); 48 | ``` 49 | 50 | 51 | ## getPassedParam() 52 | Convenience method to get passed params: 53 | ```php 54 | $param = $this->Common->getPassedParam('x', $default); 55 | ``` 56 | 57 | ## isForeignReferer() 58 | Check if a certain referer is a non local one: 59 | ```php 60 | // using env referer 61 | $result = $this->Common->isForeignReferer(); 62 | // or explicit value 63 | $result = $this->Common->isForeignReferer($urlString); 64 | ``` 65 | 66 | 67 | ## Listing actions 68 | 69 | If you need all current (direct) actions of a controller, call 70 | ```php 71 | $this->Common->listActions() 72 | ``` 73 | -------------------------------------------------------------------------------- /docs/Component/Mobile.md: -------------------------------------------------------------------------------- 1 | # Mobile component 2 | 3 | The mobile component can hold the information of whether to serve a mobile layout based on session preference and browser headers. 4 | ```php 5 | // In your controller (action) 6 | $isMobile = $this->Mobile->isMobile(); 7 | ``` 8 | 9 | You can provide a form/button in the bottom of the layout that can switch a session variable to overwrite the browser detection: 10 | Just store the user's choice in the `'User.mobile'` session key. 11 | 12 | ## Configuration 13 | 14 | 'on' => 'beforeFilter', // initialize (prior to controller's beforeRender) or startup 15 | 'engine' => null, // CakePHP internal if null 16 | 'themed' => false, // If false uses subfolders instead of themes: /View/.../mobile/ 17 | 'auto' => false, // auto set mobile views 18 | 19 | -------------------------------------------------------------------------------- /docs/Component/RefererRedirect.md: -------------------------------------------------------------------------------- 1 | # RefererRedirect component 2 | 3 | ## Why this component 4 | (Referer) Redirecting is a often wrongly/badly implemented pattern. 5 | Especially the session is here usually the worst possible usability approach. 6 | As soon as you have two tabs open they will basically kill each other, creating very weird experiences for the user. 7 | 8 | This component uses a referer key in query string to redirect back to given referer. 9 | The neat thing here is that it doesn't require changes to existing actions. This can just be 10 | added on top, for one or all controllers. 11 | 12 | Live demo: https://sandbox.dereuromark.de/sandbox/tools-examples/redirect-test 13 | 14 | ## Alternatives 15 | AN alternative is using hidden input fields, but that also requires a bit more logic in your controllers or component scope already. 16 | Hidden inputs, however, can lose their value on refresh, or if your browser restarts. So the safest bet is still to use query strings. 17 | 18 | You can also pass along all query strings always, but then you need to make sure all URLs in controllers and templates are adjusted here. 19 | And whitelisting is important. You do not want to redirect back from "removable" actions to the "view" here. 20 | 21 | ## Setting it up for a controller 22 | Let's set it up. Inside your controller: 23 | ```php 24 | /** 25 | * @return void 26 | */ 27 | public function initialize(): void { 28 | parent::initialize(); 29 | $this->loadComponent('Tools.RefererRedirect', [ 30 | 'actions' => ['edit'], 31 | ]); 32 | } 33 | ``` 34 | We whitelisted the edit action to be auto-referer redirectable. 35 | 36 | ## Adjust your links to this action 37 | 38 | From your paginated and filtered index page you can now point to the edit page like this: 39 | 40 | ```php 41 | Html->link( 42 | $this->Icon->render('edit'), 43 | ['controller' => 'Versions', 'action' => 'edit', $version->id, '?' => ['ref' => $this->getRequest()->getRequestTarget()]], 44 | ['escape' => false] 45 | ); ?> 46 | ``` 47 | 48 | After successful save it will then redirect to exactly the current URL including all filter query strings, page, sort order etc. 49 | 50 | You could also make this a RefererRedirect helper and handle that URL building internally. 51 | 52 | ## When not to use 53 | Make sure you are not using such approaches when linking to an action that removes an entity from the `view`/`edit` action of that entity. 54 | So do not use it for deleting actions while you are on the `view` action of this entity, for example. 55 | Or in that case make sure you pass the referer of the "target" action, e.g. `index` etc. This way the redirect will then not run into an error or redirect loop. 56 | 57 | ## Security 58 | It is advised to whitelist the actions for valid referer redirect. 59 | 60 | Also make sure the referer is always a local one (not pointing to external URLs). 61 | The component here itself already checks for this. 62 | -------------------------------------------------------------------------------- /docs/Contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Getting Started 4 | 5 | * Make sure you have a [GitHub account](https://github.com/signup/free) 6 | * Fork the repository on GitHub. 7 | 8 | ## Making Changes 9 | 10 | I am looking forward to your contributions. There are several ways to help out: 11 | * Write missing testcases 12 | * Write patches for bugs/features, preferably with testcases included 13 | 14 | There are a few guidelines that I need contributors to follow: 15 | * Coding standards (`composer cs-check` to check and `composer cs-fix` to fix) 16 | * Passing tests (you can enable travis to assert your changes pass) for Windows and Unix (`php phpunit.phar`) 17 | 18 | ## i18n 19 | Check if translations via pot file need to be changed. 20 | Run 21 | ``` 22 | bin/cake i18n extract -p Tools --extract-core=no --merge=no --overwrite 23 | ``` 24 | from your app. 25 | 26 | 27 | # Additional Resources 28 | 29 | * [Coding standards guide (extending/overwriting the CakePHP ones)](https://github.com/php-fig-rectified/fig-rectified-standards/) 30 | * [CakePHP coding standards](http://book.cakephp.org/3.0/en/contributing/cakephp-coding-conventions.html) 31 | * [General GitHub documentation](http://help.github.com/) 32 | * [GitHub pull request documentation](http://help.github.com/send-pull-requests/) 33 | -------------------------------------------------------------------------------- /docs/Controller/Controller.md: -------------------------------------------------------------------------------- 1 | # Controller 2 | 3 | When using the Tools plugin Controller class you can set your pagination defaults via 4 | Configure 5 | ``` 6 | 'Paginator' => [ 7 | 'limit' => ... // etc 8 | ] 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/Entity/Enum.md: -------------------------------------------------------------------------------- 1 | # Enums 2 | Since CakePHP 5 you can now use native enums. 3 | Just spice it a bit with Tools plugin magic, and all good to go. 4 | 5 | Add the `EnumOptionsTrait` to your enums to have `options()` method available in your templates etc. 6 | 7 | ```php 8 | use Tools\Model\Enum\EnumOptionsTrait; 9 | 10 | enum UserStatus: int implements EnumLabelInterface { 11 | 12 | use EnumOptionsTrait; 13 | 14 | ... 15 | 16 | } 17 | ``` 18 | 19 | If you now need the options array for some entity-less form, you can use it as such: 20 | ```php 21 | echo $this->Form->control('status', ['options' => \App\Model\Enum\UserStatus::options()]); 22 | ``` 23 | The same applies if you ever need to narrow down the options (e.g. not display some values as dropdown option), 24 | or if you want to resort the options for display: 25 | 26 | ```php 27 | $options = UserStatus::options([UserStatus::ACTIVE, UserStatus::INACTIVE]); 28 | echo $this->Form->control('status', ['options' => $options]); 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/Helper/Common.md: -------------------------------------------------------------------------------- 1 | # Common Helper 2 | 3 | A CakePHP helper to handle some common topics. 4 | 5 | ### Setup 6 | Include helper in your AppView class as 7 | ```php 8 | $this->loadHelper('Tools.Common', [ 9 | ... 10 | ]); 11 | ``` 12 | 13 | ### Singular vs Plural 14 | ```php 15 | echo $this->Common->sp('Singular', 'Plural', $count, true); 16 | ``` 17 | If using explicit translations or if no I18n translation is necessary, you don't need the 4th argument: 18 | 19 | ```php 20 | echo $this->Common->sp(__('Singular'), __('Plural'), $count); 21 | ``` 22 | 23 | ### Meta tags 24 | 25 | Canonical URL: 26 | ```php 27 | echo $this->Format->metaCanonical($url); 28 | ``` 29 | 30 | Alternate content URL: 31 | 32 | ```php 33 | echo $this->Format->metaAlternate($url, $language); 34 | ``` 35 | 36 | RSS link: 37 | ```php 38 | echo $this->Format->metaRss($url, $title); 39 | ``` 40 | 41 | Generic meta tags: 42 | 43 | ```php 44 | echo $this->Format->metaEquiv($type, $value, $escape) 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/Helper/Form.md: -------------------------------------------------------------------------------- 1 | # Form Helper 2 | 3 | An enhanced FormHelper 4 | - Allow configuration via Configure `FormConfig` 5 | - Allow easy enabling/disabling of `novalidate` this way globally throughout all forms 6 | 7 | ## Configs 8 | - 'novalidate' => false, // Set to true to disable HTML5 browser validation 9 | - 'templates' => [...] // Define your own custom default templates for all widgets 10 | 11 | ## Usage 12 | Attach it to your controllers like so: 13 | ```php 14 | protected $helpers = ['Tools.Form']; 15 | ``` 16 | 17 | Alternatively, you can enable it in your AppView class. 18 | 19 | ### Basic Example 20 | ```php 21 | // Inside your app.php config: 22 | $config = [ 23 | 'debug' => true, 24 | ... 25 | 'FormConfig' => array( 26 | 'novalidate' => true, 27 | 'templates' => array( 28 | 'dateWidget' => '{{day}}{{month}}{{year}}{{hour}}{{minute}}{{second}}{{meridian}}', 29 | ) 30 | ) 31 | ]; 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/Helper/Format.md: -------------------------------------------------------------------------------- 1 | # Format Helper 2 | 3 | A CakePHP helper to handle some common format topics. 4 | 5 | ## Setup 6 | Include helper in your AppView class as 7 | ```php 8 | $this->loadHelper('Tools.Format', [ 9 | ... 10 | ]); 11 | ``` 12 | 13 | You can store default configs also in Configure key `'Format'`. 14 | 15 | ## Usage 16 | 17 | ### yesNo() 18 | 19 | Displays yes/no symbol for e.g. boolean values as more user-friendly representation. 20 | 21 | ### ok() 22 | 23 | Display a colored result based on the 2nd argument being true or false. 24 | ```php 25 | echo $this->Format->ok($text, $bool, $optionalAttributes); 26 | ``` 27 | 28 | ### disabledLink() 29 | 30 | Display a disabled link with a default title. 31 | 32 | ### array2table() 33 | 34 | Translate a result array into a HTML table. 35 | -------------------------------------------------------------------------------- /docs/Helper/Html.md: -------------------------------------------------------------------------------- 1 | # Html Helper 2 | 3 | An enhanced HtmlHelper 4 | - imageFromBlob() 5 | - linkReset() and linkComplete() 6 | 7 | ## Usage 8 | Attach it to your controllers like so: 9 | ```php 10 | protected $helpers = ['Tools.Html']; 11 | ``` 12 | It will replace the CakePHP core one everywhere. 13 | 14 | ### Image from Blob 15 | Sometimes you might want to directly output images inside the HTML without external files involved. 16 | The image, like a small 16x16 icon, could also directly come from the database. 17 | 18 | ```php 19 | $blobImage = file_get_contents($path . 'my-image.png'); 20 | // or from somewhere else 21 | 22 | echo $this->Html->imageFromBlob($blobImage); 23 | ``` 24 | The output will be a base64 encoded embedded image. 25 | 26 | Bear in mind that this does not work with all (especially older) browsers. 27 | 28 | ### Reset and Complete Links 29 | In some cases you want to display the links without the plugin and prefix automatically being taken from the current URL. 30 | This is especially important with navigation menu or layout elements, as those will be outputted on every page, even outside of the current plugin or prefix scope. 31 | 32 | You can then either manually and verbosely always set both to `false`, or just use the convenience method: 33 | ```php 34 | echo $this->Html->linkReset(['controller' => 'Foo', 'action' => 'bar']); 35 | ``` 36 | 37 | Inside `/admin/plugin-name/example/action` the linked URL would normally become `/admin/plugin-name/foo/bar`. 38 | With the linkReset() method it will become the desired: `/foo/bar`. 39 | 40 | In both cases, however, the query strings are not passed on. If you want that, you can use the other convenience method: 41 | ```php 42 | echo $this->Html->linkComplete(['controller' => 'Foo', 'action' => 'bar']); 43 | ``` 44 | Now if there was a query string `?q=x` on the current action, it would also be passed along as `/foo/bar?q=x`. 45 | 46 | 47 | See also [Url helper](/docs/Url/Url.md). 48 | -------------------------------------------------------------------------------- /docs/Helper/Meter.md: -------------------------------------------------------------------------------- 1 | # Meter Helper 2 | 3 | A CakePHP helper to handle gauge calculation and output as meter (bar) element. 4 | By default it supports HTML5 meter element - and as alternative or fallback uses unicode chars to work completely text-based. 5 | 6 | The main advantage of the meter helper over default calculation is that you can decide on the overflow of min/max boundaries. 7 | By default the max/min borders are kept and the value just cut to this boundary value. 8 | 9 | Use the meter element to display data within a given range (a gauge). 10 | Examples: Disk usage, the relevance of a query result, etc. Fixed values basically. 11 | 12 | Note: The `` tag should not be used to indicate progress (as in a progress bar). Use Progress helper here. 13 | 14 | ## Setup 15 | Include helper in your AppView class as 16 | ```php 17 | $this->loadHelper('Tools.Meter', [ 18 | ... 19 | ]); 20 | ``` 21 | 22 | You can store default configs also in Configure key `'Meter'`. 23 | Mainly empty/full chars can be configured this way. 24 | 25 | ## Usage 26 | 27 | ### htmlMeterBar() 28 | Displays HTML5 element. 29 | This is best used with the textual fallback if you are not sure everyone is using a modern browser. 30 | See [browser support](https://www.w3schools.com/tags/tag_meter.asp). 31 | 32 | ```php 33 | echo $this->Meter->htmlMeterBar( 34 | $value, 35 | $max, 36 | $min, 37 | $options, 38 | $attributes 39 | ); 40 | ``` 41 | 42 | ### meterBar() 43 | Display a text-based progress bar with the progress in percentage as title. 44 | ```php 45 | echo $this->Meter->meterBar( 46 | $value, 47 | $max, 48 | $min, 49 | $length, // Char length >= 3 50 | $options, 51 | $attributes 52 | ); 53 | ``` 54 | 55 | ### draw() 56 | Display a text-based progress bar as raw bar. 57 | ```php 58 | echo $this->Meter->draw( 59 | $percentage, // Value 0...1 60 | $length // Char length >= 3 61 | ); 62 | ``` 63 | This can be used if you want to customize the usage. 64 | 65 | ## Tips 66 | 67 | Consider using CSS `white-space: nowrap` for the span tag if wrapping could occur to the textual version based on smaller display sizes. 68 | Wrapping would render such a text-based progress bar a bit hard to read. 69 | -------------------------------------------------------------------------------- /docs/Helper/Progress.md: -------------------------------------------------------------------------------- 1 | # Progress Helper 2 | 3 | A CakePHP helper to handle basic progress calculation and output. 4 | By default it supports HTML5 progress element - and as alternative or fallback uses unicode chars to work completely text-based. 5 | 6 | The main advantage of the progress helper over default round() calculation is that it only fully displays 7 | 0 and 100 percent borders (including the char icon representation) if truly fully that min/max value. 8 | So for `0.9999` as well as `0.0001` etc it will not yet display the completely full or empty bar. 9 | If you want that, you need to pre-round before passing it in. 10 | 11 | Tip: Use the `` tag in conjunction with JavaScript to display the progress of a task. 12 | 13 | Note: The `` tag is not suitable for representing a gauge (e.g. disk space usage or relevance of a query result). 14 | To represent a gauge, use the Meter helper instead. 15 | 16 | ## Setup 17 | Include helper in your AppView class as 18 | ```php 19 | $this->loadHelper('Tools.Progress', [ 20 | ... 21 | ]); 22 | ``` 23 | 24 | You can store default configs also in Configure key `'Progress'`. 25 | Mainly empty/full chars can be configured this way. 26 | 27 | ## Usage 28 | 29 | ### htmlProgressBar() 30 | Displays HTML5 element. 31 | This is best used with the textual fallback if you are not sure everyone is using a modern browser. 32 | See [browser support](https://www.w3schools.com/tags/tag_progress.asp). 33 | 34 | ### progressBar() 35 | Display a text-based progress bar with the progress in percentage as title. 36 | ```php 37 | echo $this->Progress->progressBar( 38 | $percentage // Value 0...1 39 | $length, // Char length >= 3 40 | $attributes 41 | ); 42 | ``` 43 | 44 | ### draw() 45 | Display a text-based progress bar as raw bar. 46 | ```php 47 | echo $this->Progress->draw( 48 | $percentage // Value 0...1 49 | $length // Char length >= 3 50 | ); 51 | ``` 52 | This can be used if you want to customize the usage. 53 | 54 | ### calculatePercentage() 55 | 56 | This method is responsible for the main percentage calculation. 57 | It can be also used standalone. 58 | ```php 59 | $percentage = $this->Progress->calculatePercentage($total, $is); 60 | echo $this->Number->toPercentage($percentage, 0, ['multiply' => true]); 61 | ``` 62 | 63 | ### roundPercentage() 64 | 65 | This method is responsible for the above min/max handling. 66 | It can be also used standalone. 67 | ```php 68 | $percentage = $this->Progress->roundPercentage($value); 69 | echo $this->Number->toPercentage($percentage, 0, ['multiply' => true]); 70 | ``` 71 | For value `0.49` it outputs: `49%`, for value `0.0001` it outputs `1%`. 72 | And of course `0.99999` should still be "only" `99%`. 73 | 74 | ## Tips 75 | 76 | Consider using CSS `white-space: nowrap` for the span tag if wrapping could occur to the textual version based on smaller display sizes. 77 | Wrapping would render such a text-based progress bar a bit hard to read. 78 | -------------------------------------------------------------------------------- /docs/Helper/Typography.md: -------------------------------------------------------------------------------- 1 | # Typography Helper 2 | 3 | A CakePHP helper to handle typographic consistency. 4 | 5 | The basic idea is to normalize all input into a standard typography (utf8 default). 6 | So different quotes like `»` or `“` end up as `"` in the database. 7 | See the [TypographicBehavior](/docs/Behavior/Typographic.md) docs for input modification. 8 | 9 | Upon output one can the decide to re-apply localization here. 10 | 11 | ### Usage 12 | 13 | #### Basic usage 14 | Include helper in your AppView class as 15 | ```php 16 | $this->loadHelper('Tools.Typography', [ 17 | ... 18 | ]); 19 | ``` 20 | 21 | Then you can use it in your templates as 22 | ```php 23 | echo $this->Typography->autoTypography($myText); 24 | ``` 25 | 26 | ### Configuration 27 | 28 | It uses Configure key `'App.language'` by default to detect the output format. 29 | So if you have `Configure::write('App.language', 'deu');` in your bootstrap, it will use German typography. 30 | A string `"Interesting Quote"` will then become the German `„Interesting Quote‟`. 31 | English (US) would be `“Interesting Quote”` and French, for example, `«Interesting Quote»`. 32 | -------------------------------------------------------------------------------- /docs/I18n/DateTime.md: -------------------------------------------------------------------------------- 1 | # DateTime and Date 2 | 3 | This package ships with improved 4 | - DateTime 5 | - Date 6 | 7 | extending the core value objects. 8 | 9 | They can also work with select/dropdown date(time) form controls and provide other improvements. 10 | 11 | ## Validation 12 | 13 | For improved validation, the Shim Table - that you can extend in your tables - ships with 14 | - `validateDateTime()` 15 | - `validateDate()` 16 | - `validateTime()` 17 | 18 | They provide additional configs to work with: 19 | - `timeFormat` 20 | - `after`/`before` (field name to validate against) 21 | - `min`/`max` 22 | -------------------------------------------------------------------------------- /docs/Install.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## How to include 4 | Installing the Plugin is pretty much as with every other CakePHP Plugin. 5 | 6 | ``` 7 | composer require dereuromark/cakephp-tools 8 | ``` 9 | 10 | The following command can enable the plugin: 11 | ``` 12 | bin/cake plugin load Tools 13 | ``` 14 | 15 | ## Namespacing 16 | Using Cake3 and namespaces, don't forget to add "Tools" as namespace to new files. 17 | Also don't forget the `use` statements. 18 | 19 | If you create a new behavior in the plugin, it might look like this: 20 | ```php 21 | namespace Tools\Model\Behavior; 22 | 23 | use Cake\ORM\Behavior; 24 | 25 | class CoolBehavior extends Behavior { 26 | } 27 | ``` 28 | 29 | For a new APP behavior "MySlugged" that extends "Tools.Slugged" it is: 30 | ```php 31 | namespace App\Model\Behavior; 32 | 33 | use Tools\Model\Behavior\SluggedBehavior; 34 | 35 | class MySluggedBehavior extends SluggedBehavior { 36 | } 37 | ``` 38 | Note that use statements should be in alphabetical order. 39 | See CakePHP coding standards for details. 40 | 41 | ### Internal handling via plugin dot notation 42 | Internally (method access), you don't use the namespace declaration. The plugin name suffices: 43 | ```php 44 | // In a Table 45 | $this->addBehavior('Tools.Slugged'); // Adding SluggedBehavior 46 | 47 | // In a View 48 | $this->loadHelper('Tools.Foo'); // Adding FooHelper 49 | 50 | // In a Controller (deprecated) 51 | protected $helpers = ['Tools.Foo']; // Adding FooHelper 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/Model/Table.md: -------------------------------------------------------------------------------- 1 | # Table 2 | 3 | The table class provides additional validation you can use: 4 | 5 | - `validateUniqueExt()` to allow validating with multiple scoped fields with NULL values. 6 | - `validateIdentical()` 7 | - `validateDateTime()` with before/after. 8 | - `validateDate()` with before/after. 9 | - `validateTime()` with before/after. 10 | - `validateUrl()` with auto-complete/deep. 11 | 12 | ## Related values in use 13 | 14 | For pagination and filtering you often want to display selects. 15 | Those would often then contain all possible values from the related table. 16 | 17 | But for better UI/UX you usually don't want to display the values that yield in no results. 18 | Only the ones actually used in the current context should be selectable. 19 | 20 | `getRelatedInUse()` is doing that: 21 | 22 | ```php 23 | $authors = $this->Posts->getRelatedInUse('Authors', 'author_id', 'list') 24 | ->toArray(); 25 | ``` 26 | 27 | For optional columns used for relations, make sure to set IS NOT NULL conditions here. 28 | ```php 29 | ->getRelatedInUse('Related', null, 'list', ['conditions' => ['optional_relation_id IS NOT' => null]]) 30 | ->toArray(); 31 | ``` 32 | 33 | 34 | ## Truncate 35 | 36 | `truncate()` is a convenience wrapper to truncate a table. 37 | -------------------------------------------------------------------------------- /docs/Model/Tokens.md: -------------------------------------------------------------------------------- 1 | # Tokens 2 | 3 | Easily easily manage (store, retrieve, validate) tokens. 4 | They are useful in the registration process of users, 5 | or if you want to send some double-opt-in confirmation emails, for example. 6 | 7 | The main methods of the model are 8 | * newKey(string $type, ?string $key = null, $uid = null, $content = null) 9 | * useKey(string $type, string $key, $uid = null) 10 | * spendKey(int $id) 11 | 12 | User and security relevant token usage should always be bound to the id of the user (user_id). 13 | Here you should also use one-time tokens only. 14 | 15 | Other operations can also omit this field. 16 | Here you could also use unlimited tokens if needed. 17 | 18 | ## Install 19 | ``` 20 | bin/cake migrations migrate -p Tools 21 | ``` 22 | If you need a different table schema, e.g. for user_id to be UUID, you can copy 23 | over the migration file and customize. In that case execute it then without the plugin option. 24 | 25 | ## Usage for registration 26 | 27 | ### Register action 28 | ```php 29 | $this->loadModel('Tools.Tokens'); 30 | $tokenKey = $this->Tokens->newKey('activate', null, $user->id); 31 | ``` 32 | 33 | As 4th parameter any string content can be stored. 34 | 35 | ### Activate action 36 | ```php 37 | $this->loadModel('Tools.Tokens'); 38 | $token = $this->Tokens->useKey('activate', $tokenKey); 39 | 40 | if ($token && $token->used) { 41 | $this->Flash->warning(__('Already activated')); 42 | } elseif ($token) { 43 | $uid = $token->user_id; 44 | // Confirm activation and redirect to home 45 | } 46 | ``` 47 | 48 | ## Other usage 49 | 50 | ### Changing email 51 | Here the 4th argument comes in handy. 52 | The new email address one will be stored in content until 53 | validation is complete and will then replace the old one. 54 | 55 | ### One time email links 56 | Login or otherwise. 57 | 58 | 59 | ## Garbage Collect 60 | From Cronjob/CLI 61 | ```php 62 | $this->loadModel('Tools.Tokens'); 63 | $this->Tokens->garbageCollector(); 64 | ``` 65 | 66 | ## Stats 67 | There is also a method `stats()` to retrieve statistics if required/useful to you. 68 | 69 | ## Security notes 70 | By default, the tokens have a validity of one week. 71 | You can modify this value in your model. 72 | 73 | Do not send plain passwords with your emails or print them out anywhere. 74 | That's why you should send the expiring tokens. 75 | 76 | If you feel like you need more information on the implementation process, 77 | read [this article at troyhunt.com](http://www.troyhunt.com/2012/05/everything-you-ever-wanted-to-know.html). 78 | It describes in a very verbose way what to do and what better not to do. 79 | -------------------------------------------------------------------------------- /docs/Shims.md: -------------------------------------------------------------------------------- 1 | # Migration from 4.x to 5.x: Shims 2 | Shims ease migration as complete parts of the code, such as validation and other model property settings 3 | can be reused immediately without refactoring them right away. 4 | 5 | See the [Shim plugin](https://github.com/dereuromark/cakephp-shim) for details. 6 | 7 | Note: It does not hurt to have them, if you don't use them. The overhead is minimal. 8 | 9 | ### Entity 10 | - Enums via enum() are ported in entity, if you used them before. 11 | -------------------------------------------------------------------------------- /docs/Upgrade.md: -------------------------------------------------------------------------------- 1 | # Migration from 4.x to 5.x 2 | 3 | ## CommonComponent 4 | - `setHelpers()` has been removed in favor of core usage directly. 5 | 6 | ## Auth 7 | - MultiColumn authentication has fully been moved to [TinyAuth](https://github.com/dereuromark/cakephp-tinyauth) plugin. 8 | 9 | ## Utility 10 | - `L10n`, `Mime` classes have been removed 11 | - Mutable `Time` class has been removed, use immutable `DateTime` instead. 12 | - `Number` class has been moved from Utility to I18n namespace. 13 | - `DateTime` class has been moved from Utility to I18n namespace. 14 | 15 | ## NumberHelper 16 | - Custom engine has been moved from Utility to I18n namespace. 17 | 18 | ## TimeHelper 19 | - Custom engine has been moved from Utility to I18n namespace. 20 | -------------------------------------------------------------------------------- /docs/Url/Url.md: -------------------------------------------------------------------------------- 1 | # Url component and helper 2 | 3 | There is both a component and helper that help to work around some URL issues. 4 | 5 | ### Defaults 6 | If you need to merge in defaults to your URLs, you can get the information from the `defaults()` method: 7 | 8 | ```php 9 | // From inside a plugin controller action 10 | $this->redirect(['controller' => 'Main', 'action' => 'index'] + $this->Url->defaults()); 11 | ``` 12 | It will basically add in `'prefix' => false, 'plugin' => false`. 13 | 14 | ### Reset 15 | You can in that case also just use the convenience method: 16 | ```php 17 | $url = $this->Url->buildReset(['controller' => 'Main', 'action' => 'overview']); 18 | ``` 19 | 20 | In case you just want the array (to pass it on), use: 21 | ```php 22 | // Inside an action 23 | $urlArray = $this->Url->resetArray(['controller' => 'Main', 'action' => 'overview']); 24 | return $this->redirect($urlArray); 25 | ``` 26 | 27 | Inside `/admin/plugin-name/example/action` the URL to redirect to would normally become `/admin/plugin-name/main/overview`. 28 | With the reset() method it will become the desired: `/main/overview`. 29 | 30 | For the controller 31 | 32 | ### Complete 33 | In both cases, however, the query strings are not passed on. If you want that, you can use the other convenience method: 34 | ```php 35 | $url = $this->Url->buildComplete(['controller' => 'Main', 'action' => 'overview']); 36 | ``` 37 | 38 | In case you just want the array (to pass it on), use: 39 | ```php 40 | // Inside an action 41 | $urlArray = $this->Url->completeArray(['controller' => 'Main', 'action' => 'overview']); 42 | return $this->redirect($urlArray); 43 | ``` 44 | 45 | Now if there was a query string `?q=x` on the current action, it would also be passed along as `/main/overview?q=x`. 46 | 47 | 48 | ### Generating links 49 | For generating links for those cases please see [Html helper](/docs/Helper/Html.md). 50 | -------------------------------------------------------------------------------- /docs/Utility/FileLog.md: -------------------------------------------------------------------------------- 1 | # FileLog 2 | 3 | Log class let's you write logs into custom log files. 4 | 5 | #### With default `Cake\Log\Log` class: 6 | 7 | To write log data into custom file with default CakePHP `Cake\Log\Log` class feels like repeating ourselves. 8 | 9 | 1. Configure FileLog Adapter: 10 | ```php 11 | use Cake\Log\Log; 12 | 13 | Log::config('custom_file', [ 14 | 'className' => 'File', 15 | 'path' => LOGS, 16 | 'levels' => ['debug'], 17 | 'file' => 'my_file.log', 18 | ]); 19 | ``` 20 | 21 | 2. Write logs into file: 22 | ```php 23 | Log::write('debug', "Something didn't work!"); 24 | ``` 25 | 26 | With above approach, we have multiple issues: 27 | - It surely logs data into `custom_file.log` file but also it will log into level specific `$level.log` file too, so we end up duplicating the log data. 28 | - When we have to write more logs into same file from different parts of the project, we have to copy-paste this code every time. 29 | 30 | Or you hack it with doing something like setting configurations in `bootstrap.php` and use scope to log data. But each time you start new project you have to remember to copy paste that config and use in your project in order to write data into custom log files. 31 | 32 | #### With `Tools\Utility\FileLog` class: 33 | 34 | You can directly pass data to log and filename to write the data into. 35 | 36 | ##### Usage: 37 | 38 | ```php 39 | use Tools\Utility\FileLog; 40 | 41 | FileLog::write("Something didn't work!", 'my_file'); 42 | 43 | // Somewhere else in any file 44 | FileLog::write([ 45 | 'user' => [ 46 | 'id' => '1', 47 | 'name' => 'John', 48 | 'email' => 'john@example.com', 49 | ] 50 | ], 'user_data'); 51 | ``` 52 | 53 | That's it! Above will create two separate files in `log/` directory named `my_file.log` and `user_data.log` store data into which we passed in first argument. By default if you don't pass the `$filename` in second param in `FileLog::write` method, it will create `custom_log.log` file. 54 | 55 | You can write string, array, objects, etc into log files. It will pretty print your array/object so it's more readable. Also, it will not duplicate records into `$level.log` file. 56 | -------------------------------------------------------------------------------- /docs/Widget/Datalist.md: -------------------------------------------------------------------------------- 1 | # Datalist Widget 2 | 3 | Many of the HTML 5 new widgets are automatically supported by CakePHP. 4 | Unfortunatelly [datalist](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist) is not supported by default. 5 | 6 | This widget adds support for basic datalist support using the values or keys of the options array. 7 | If you need shimming for older browsers, add your JS snippet for this as polyfill yourself. 8 | 9 | ### Setup 10 | 11 | Enable the following widget in your Form helper config: 12 | ```php 13 | 'datalist' => ['Tools\View\Widget\DatalistWidget'], 14 | ``` 15 | 16 | Add the following template to your Form helper templates: 17 | ```php 18 | 'datalist' => '{{content}}', 19 | ``` 20 | 21 | Config: 22 | - keys: Use as true to use the keys of the select options instead of the values. 23 | - input: Attributes for input element 24 | 25 | ### Usage 26 | 27 | ```php 28 | echo $this->Form->control('search', ['type' => 'datalist', 'options' => $options]); 29 | ``` 30 | 31 | It will generate the above input with a datalist element. 32 | 33 | If you want to use the keys instead of the values: 34 | ```php 35 | echo $this->Form->control('search', ['type' => 'datalist', 'options' => $options, 'keys' => true]); 36 | ``` 37 | 38 | You can pass input attributes using the `'input'` key, e.g. 39 | `'input' => ['placeholder' => 'My placeholder']`. 40 | 41 | ### Advanced usage 42 | You could also auto-create new entries right away. 43 | See [rrd108/cakephp-datalist](https://github.com/rrd108/cakephp-datalist) for this. 44 | 45 | Credits for this widget go to him for discovering the basics here. 46 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | src/ 8 | config/ 9 | tests/ 10 | 11 | /tests/test_files/ 12 | /tests/test_app/ 13 | 14 | 15 | 16 | 17 | */config/Migrations/* 18 | 19 | 20 | */config/Migrations/* 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | paths: 4 | - src/ 5 | treatPhpDocTypesAsCertain: false 6 | bootstrapFiles: 7 | - %rootDir%/../../../tests/bootstrap.php 8 | excludePaths: 9 | - %rootDir%/../../../src/TestSuite/* 10 | - %rootDir%/../../../src/Utility/Mime.php 11 | - %rootDir%/../../../src/View/Helper/QrCodeHelper.php 12 | reportUnmatchedIgnoredErrors: false 13 | ignoreErrors: 14 | - identifier: missingType.generics 15 | - identifier: missingType.iterableValue 16 | - identifier: trait.unused 17 | - identifier: method.childParameterType 18 | - 19 | message: '#Undefined variable: .+#' 20 | path: '%rootDir%/../../../src/View/Helper/TreeHelper.php' 21 | - 22 | message: '#Variable \$.+ might not be defined.#' 23 | path: '%rootDir%/../../../src/View/Helper/TreeHelper.php' 24 | - 25 | message: '#Negated boolean expression is always true.#' 26 | path: '%rootDir%/../../../src/View/Helper/TreeHelper.php' 27 | - 28 | message: '#Result of \|\| is always true.#' 29 | path: '%rootDir%/../../../src/View/Helper/TreeHelper.php' 30 | - 31 | message: '#Cannot unset offset string on array.+\.#' 32 | path: '%rootDir%/../../../src/Utility/Language.php' 33 | - 34 | message: '#Instanceof between mixed and .+ComparisonExpression will always evaluate to false\.#' 35 | path: '%rootDir%/../../../src/Model/Behavior/BitmaskedBehavior.php' 36 | - '#Parameter \#4 \$flags of function preg_match expects TFlags of 0\|256\|512\|768, int given.#' 37 | - '#Parameter \#2 \$\w+ of function datefmt_format expects .+, DateTimeInterface given.#' 38 | - '#Unsafe usage of new static\(\)#' 39 | -------------------------------------------------------------------------------- /resources/locales/README.txt: -------------------------------------------------------------------------------- 1 | The .pot (template) file contains the translated strings. 2 | Copy them to your existing .po file in 3 | ROOT/resources/locales/[language]/tools.po 4 | to customize. 5 | 6 | For some languages there might be already out of the box translations available. 7 | If not, you can send them to me and I will add them. 8 | -------------------------------------------------------------------------------- /src/Auth/AbstractPasswordHasher.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | protected array $_defaultConfig = []; 22 | 23 | /** 24 | * Constructor 25 | * 26 | * @param array $config Array of config. 27 | */ 28 | public function __construct(array $config = []) { 29 | $this->setConfig($config); 30 | } 31 | 32 | /** 33 | * Generates password hash. 34 | * 35 | * @param string $password Plain text password to hash. 36 | * @return string The password hash 37 | */ 38 | abstract public function hash(string $password): string; 39 | 40 | /** 41 | * Check hash. Generate hash from user provided password string or data array 42 | * and check against existing hash. 43 | * 44 | * @param string $password Plain text password to hash. 45 | * @param string $hashedPassword Existing hashed password. 46 | * @return bool True if hashes match else false. 47 | */ 48 | abstract public function check(string $password, string $hashedPassword): bool; 49 | 50 | /** 51 | * Returns true if the password need to be rehashed, due to the password being 52 | * created with anything else than the passwords generated by this class. 53 | * 54 | * Returns true by default since the only implementation users should rely 55 | * on is the one provided by default in php 5.5+ or any compatible library 56 | * 57 | * @param string $password The password to verify 58 | * @return bool 59 | */ 60 | public function needsRehash(string $password): bool { 61 | return password_needs_rehash($password, PASSWORD_DEFAULT); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/Auth/DefaultPasswordHasher.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | protected array $_defaultConfig = [ 24 | 'hashType' => PASSWORD_DEFAULT, 25 | 'hashOptions' => [], 26 | ]; 27 | 28 | /** 29 | * Generates password hash. 30 | * 31 | * @psalm-suppress InvalidNullableReturnType 32 | * @link https://book.cakephp.org/4/en/controllers/components/authentication.html#hashing-passwords 33 | * @param string $password Plain text password to hash. 34 | * @return string Password hash or false on failure 35 | */ 36 | public function hash(string $password): string { 37 | return password_hash( 38 | $password, 39 | $this->_config['hashType'], 40 | $this->_config['hashOptions'], 41 | ); 42 | } 43 | 44 | /** 45 | * Check hash. Generate hash for user provided password and check against existing hash. 46 | * 47 | * @param string $password Plain text password to hash. 48 | * @param string $hashedPassword Existing hashed password. 49 | * @return bool True if hashes match else false. 50 | */ 51 | public function check(string $password, string $hashedPassword): bool { 52 | return password_verify($password, $hashedPassword); 53 | } 54 | 55 | /** 56 | * Returns true if the password need to be rehashed, due to the password being 57 | * created with anything else than the passwords generated by this class. 58 | * 59 | * @param string $password The password to verify 60 | * @return bool 61 | */ 62 | public function needsRehash(string $password): bool { 63 | return password_needs_rehash($password, $this->_config['hashType'], $this->_config['hashOptions']); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/Auth/PasswordHasherFactory.php: -------------------------------------------------------------------------------- 1 | |string $passwordHasher Name of the password hasher or an array with 17 | * at least the key `className` set to the name of the class to use 18 | * @throws \RuntimeException If password hasher class not found or 19 | * it does not extend {@link \Tools\Auth\AbstractPasswordHasher} 20 | * @return \Tools\Auth\AbstractPasswordHasher Password hasher instance 21 | */ 22 | public static function build($passwordHasher): AbstractPasswordHasher { 23 | $config = []; 24 | if (is_string($passwordHasher)) { 25 | $class = $passwordHasher; 26 | } else { 27 | $class = $passwordHasher['className']; 28 | $config = $passwordHasher; 29 | unset($config['className']); 30 | } 31 | 32 | $className = App::className('Tools.' . $class, 'Auth', 'PasswordHasher'); 33 | if ($className === null) { 34 | throw new RuntimeException(sprintf('Password hasher class "%s" was not found.', $class)); 35 | } 36 | 37 | $hasher = new $className($config); 38 | if (!($hasher instanceof AbstractPasswordHasher)) { 39 | throw new RuntimeException('Password hasher must extend AbstractPasswordHasher class.'); 40 | } 41 | 42 | return $hasher; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/Authenticator/LoginLinkAuthenticator.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | protected array $_defaultConfig = [ 18 | 'urlChecker' => null, // e.g. 'Authentication.CakeRouter', 19 | 'loginUrl' => null, 20 | 'oneTime' => true, 21 | 'queryString' => 'token', 22 | 'identifierKey' => 'id', 23 | ]; 24 | 25 | /** 26 | * @param \Cake\Http\ServerRequest $request The request that contains login information. 27 | * @return \Authentication\Authenticator\ResultInterface 28 | */ 29 | public function authenticate(ServerRequestInterface $request): ResultInterface { 30 | $token = $this->getToken($request); 31 | if (!$token) { 32 | return new Result(null, ResultInterface::FAILURE_CREDENTIALS_MISSING); 33 | } 34 | 35 | $user = $this->getUserFromToken($token); 36 | 37 | if (!$user) { 38 | return new Result(null, ResultInterface::FAILURE_IDENTITY_NOT_FOUND, $this->_identifier->getErrors()); 39 | } 40 | 41 | return new Result($user, ResultInterface::SUCCESS); 42 | } 43 | 44 | /** 45 | * @param \Cake\Http\ServerRequest $request 46 | * @return string|null The token or null. 47 | */ 48 | protected function getToken(ServerRequestInterface $request): ?string { 49 | /** @var array $url */ 50 | $url = $this->getConfig('loginUrl'); 51 | if ($url) { 52 | if (is_string($url) && $url !== $request->getUri()->getPath()) { 53 | return null; 54 | } 55 | 56 | if (is_array($url)) { 57 | $params = $request->getAttribute('params'); 58 | if (!$params || $params['controller'] !== $url['controller'] || $params['action'] !== $url['action']) { 59 | return null; 60 | } 61 | } 62 | } 63 | 64 | return $request->getQuery('token'); 65 | } 66 | 67 | /** 68 | * @param string $token 69 | * @return \Cake\Datasource\EntityInterface|null 70 | */ 71 | protected function getUserFromToken(string $token): ?EntityInterface { 72 | /** @var \Tools\Model\Table\TokensTable $tokensTable */ 73 | $tokensTable = TableRegistry::getTableLocator()->get('Tools.Tokens'); 74 | $tokenEntity = $tokensTable->useKey('login_link', $token, null, (bool)$this->getConfig('oneTime')); 75 | if (!$tokenEntity) { 76 | return null; 77 | } 78 | 79 | /** @var \Cake\ORM\Entity $identity */ 80 | $identity = $this->_identifier->identify([$this->getConfig('identifierKey') => $tokenEntity->user_id]); 81 | $email = $tokenEntity->content; 82 | if ($email && $identity && $identity->get('email') && $email !== $identity->get('email')) { 83 | return null; 84 | } 85 | 86 | return $identity; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/Controller/Admin/PagesController.php: -------------------------------------------------------------------------------- 1 | read(); 19 | $files = $folders[1]; 20 | 21 | $pages = []; 22 | foreach ($files as $file) { 23 | if (substr($file, -4) !== '.php') { 24 | continue; 25 | } 26 | $page = substr($file, 0, -4); 27 | $pages[$page] = [ 28 | 'label' => Inflector::humanize($page), 29 | 'action' => Inflector::variable($page), 30 | ]; 31 | } 32 | 33 | $this->set(compact('pages')); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/Controller/Admin/ToolsController.php: -------------------------------------------------------------------------------- 1 | viewBuilder()->addHelper('Tools.Format'); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/Controller/Component/RefererRedirectComponent.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | protected array $_defaultConfig = [ 29 | 'actions' => [], 30 | ]; 31 | 32 | /** 33 | * @param \Cake\Event\EventInterface $event 34 | * @param array|string $url A string or array containing the redirect location 35 | * @param \Cake\Http\Response $response The response object. 36 | * 37 | * @return void 38 | */ 39 | public function beforeRedirect(EventInterface $event, $url, Response $response): void { 40 | $actions = $this->getConfig('actions'); 41 | $currentAction = $this->getController()->getRequest()->getParam('action'); 42 | 43 | if ($currentAction && $actions && !in_array($currentAction, $actions, true)) { 44 | return; 45 | } 46 | 47 | $referer = $this->referer(); 48 | if (!$referer) { 49 | return; 50 | } 51 | 52 | $event->setResult($response->withLocation($referer)); 53 | } 54 | 55 | /** 56 | * Only accept relative URLs. 57 | * 58 | * @see \Cake\Http\ServerRequest::referer() 59 | * 60 | * @return string|null 61 | */ 62 | protected function referer(): ?string { 63 | $referer = $this->getController()->getRequest()->getQuery(static::QUERY_REFERER); 64 | if (!$referer) { 65 | return null; 66 | } 67 | 68 | if (is_array($referer)) { 69 | $referer = Router::url($referer); 70 | } 71 | 72 | if (!is_string($referer) || !str_starts_with($referer, '/')) { 73 | return null; 74 | } 75 | 76 | return $referer; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/Controller/Controller.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public array $autoRedirectActions = []; 21 | 22 | /** 23 | * Handles automatic pagination of model records. 24 | * 25 | * @override To support defaults like limit etc. 26 | * 27 | * @param \Cake\Datasource\RepositoryInterface|\Cake\Datasource\QueryInterface|string|null $object Table to paginate 28 | * (e.g: Table instance, 'TableName' or a Query object) 29 | * @param array $settings The settings/configuration used for pagination. See {@link \Cake\Controller\Controller::$paginate}. 30 | * @return \Cake\Datasource\Paging\PaginatedInterface 31 | */ 32 | public function paginate( 33 | RepositoryInterface|QueryInterface|string|null $object = null, 34 | array $settings = [], 35 | ): PaginatedInterface { 36 | $defaultSettings = (array)Configure::read('Paginator'); 37 | if ($defaultSettings) { 38 | $this->paginate += $defaultSettings; 39 | } 40 | 41 | return parent::paginate($object, $settings); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/Controller/ShuntRequestController.php: -------------------------------------------------------------------------------- 1 | ['locale' => 'de_DE', 'name' => 'Deutsch'], ... 20 | * 21 | * @property \Cake\Controller\Component\FlashComponent $Flash 22 | */ 23 | class ShuntRequestController extends AppController { 24 | 25 | /** 26 | * @return void 27 | */ 28 | public function initialize(): void { 29 | parent::initialize(); 30 | 31 | if (!isset($this->Flash)) { 32 | $this->loadComponent('Flash'); 33 | } 34 | } 35 | 36 | /** 37 | * Switch language as post link. 38 | * 39 | * @param string|null $language 40 | * @throws \RuntimeException 41 | * @return \Cake\Http\Response 42 | */ 43 | public function language($language = null) { 44 | $this->getRequest()->allowMethod(['post']); 45 | 46 | $allowedLanguages = (array)Configure::read('Config.allowedLanguages'); 47 | if (!$language) { 48 | $language = Configure::read('Config.defaultLanguage'); 49 | } 50 | if (!$language && $allowedLanguages) { 51 | $keys = array_keys($allowedLanguages); 52 | $language = $allowedLanguages[array_shift($keys)]; 53 | } 54 | 55 | if (!array_key_exists($language, $allowedLanguages)) { 56 | throw new RuntimeException('Invalid Language'); 57 | } 58 | $language = $allowedLanguages[$language]; 59 | 60 | $this->getRequest()->getSession()->write('Config.language', $language['locale']); 61 | I18n::setLocale($language['locale']); 62 | $this->Flash->success(__d('tools', 'Language switched to {0}', $language['name'])); 63 | 64 | /** @var \Cake\Http\Response */ 65 | return $this->redirect($this->referer()); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/Error/ErrorLogger.php: -------------------------------------------------------------------------------- 1 | getConfig('skipLog') as $class) { 29 | if ($exception instanceof $class) { 30 | return; 31 | } 32 | } 33 | 34 | if ($this->is404($exception)) { 35 | $level = LOG_ERR; 36 | $message = $this->getMessage($exception); 37 | 38 | if ($request !== null) { 39 | $message .= $this->getRequestContext($request); 40 | } 41 | 42 | $message .= "\n\n"; 43 | 44 | Log::write($level, $message, ['404']); 45 | 46 | return; 47 | } 48 | 49 | parent::logException($exception, $request); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/Error/Middleware/ErrorHandlerMiddleware.php: -------------------------------------------------------------------------------- 1 | exceptionTrap = new ExceptionTrap($this->getConfig()); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/Form/ContactForm.php: -------------------------------------------------------------------------------- 1 | addField('name', ['type' => 'string', 'length' => 40]) 24 | ->addField('email', ['type' => 'string', 'length' => 50]) 25 | ->addField('subject', ['type' => 'string', 'length' => 60]) 26 | ->addField('body', ['type' => 'text']); 27 | } 28 | 29 | /** 30 | * @param \Cake\Validation\Validator $validator 31 | * @return \Cake\Validation\Validator 32 | */ 33 | public function validationDefault(Validator $validator): Validator { 34 | return $validator 35 | ->requirePresence('name') 36 | ->notEmptyString('name', __d('tools', 'This field cannot be left empty')) 37 | ->requirePresence('email') 38 | ->add('email', 'format', [ 39 | 'rule' => 'email', 40 | 'message' => __d('tools', 'A valid email address is required'), 41 | ]) 42 | ->requirePresence('subject') 43 | ->notEmptyString('subject', __d('tools', 'This field cannot be left empty')) 44 | ->requirePresence('body') 45 | ->notEmptyString('body', __d('tools', 'This field cannot be left empty')); 46 | } 47 | 48 | /** 49 | * @param array $data 50 | * @return bool 51 | */ 52 | protected function _execute(array $data): bool { 53 | // Overwrite in your extending class 54 | return true; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/I18n/Date.php: -------------------------------------------------------------------------------- 1 | $time 9 | * @return string 10 | */ 11 | public static function constructDate(array $time): string { 12 | $format = ''; 13 | if ( 14 | isset($time['year'], $time['month'], $time['day']) && 15 | (is_numeric($time['year']) && is_numeric($time['month']) && is_numeric($time['day'])) 16 | ) { 17 | $format = sprintf('%d-%02d-%02d', $time['year'], $time['month'], $time['day']); 18 | } 19 | 20 | return $format; 21 | } 22 | 23 | /** 24 | * @param array $time 25 | * @return string 26 | */ 27 | public static function constructTime(array $time): string { 28 | $format = ''; 29 | $time += [ 30 | 'minute' => 0, 31 | 'second' => 0, 32 | ]; 33 | 34 | if ( 35 | isset($time['hour']) && 36 | (is_numeric($time['hour']) && is_numeric($time['minute']) && is_numeric($time['second'])) 37 | ) { 38 | $format = sprintf('%02d:%02d:%02d', $time['hour'], $time['minute'], $time['second']); 39 | } 40 | 41 | return $format; 42 | } 43 | 44 | /** 45 | * @param array $time 46 | * @return string 47 | */ 48 | public static function constructDatetime(array $time): string { 49 | $date = static::constructDate($time); 50 | $time = static::constructTime($time); 51 | if (!$date) { 52 | return ''; 53 | } 54 | if (!$time) { 55 | return $date . ' 00:00:00'; 56 | } 57 | 58 | return $date . ' ' . $time; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/Identifier/LoginLinkIdentifier.php: -------------------------------------------------------------------------------- 1 | 'id', 23 | 'dataField' => 'id', 24 | 'resolver' => 'Authentication.Orm', 25 | 'preCallback' => null, // Use to set email to active if needed for resolver 26 | ]; 27 | 28 | /** 29 | * @inheritDoc 30 | */ 31 | public function identify(array $credentials): ArrayAccess|array|null { 32 | $dataField = $this->getConfig('dataField'); 33 | if (!isset($credentials[$dataField])) { 34 | return null; 35 | } 36 | 37 | /** @var callable|null $callback */ 38 | $callback = $this->getConfig('preCallback'); 39 | if ($callback !== null) { 40 | $callback($credentials[$dataField]); 41 | } 42 | 43 | $conditions = [ 44 | $this->getConfig('idField') => $credentials[$dataField], 45 | ]; 46 | 47 | return $this->getResolver()->find($conditions); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/Mailer/MailerAwareTrait.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | protected array $_defaultConfig = [ 31 | ]; 32 | 33 | /** 34 | * @param \Cake\Event\EventInterface $event 35 | * @param \Cake\Datasource\EntityInterface $entity 36 | * @param \ArrayObject $options 37 | * @return void 38 | */ 39 | public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options): void { 40 | $this->_entity = clone $entity; 41 | } 42 | 43 | /** 44 | * @throws \LogicException 45 | * @return \Cake\Datasource\EntityInterface 46 | */ 47 | public function getEntityBeforeSave() { 48 | if (!$this->_entity) { 49 | throw new LogicException('You need to successfully save first - including beforeSave() callback fired.'); 50 | } 51 | 52 | return $this->_entity; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/Model/Behavior/ConfirmableBehavior.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | protected array $_defaultConfig = [ 26 | 'message' => null, 27 | 'field' => 'confirm', 28 | 'validator' => 'default', 29 | ]; 30 | 31 | /** 32 | * @param \Cake\ORM\Table $table 33 | * @param array $config 34 | */ 35 | public function __construct(Table $table, array $config = []) { 36 | parent::__construct($table, $config); 37 | 38 | if (!$this->_config['message']) { 39 | $this->_config['message'] = __d('tools', 'Please confirm the checkbox'); 40 | } 41 | } 42 | 43 | /** 44 | * @param \Cake\Event\EventInterface $event 45 | * @param \Cake\Validation\Validator $validator 46 | * @param string $name 47 | * @return void 48 | */ 49 | public function buildValidator(EventInterface $event, Validator $validator, $name) { 50 | $this->build($validator, $name); 51 | } 52 | 53 | /** 54 | * @param \Cake\Validation\Validator $validator 55 | * @param string $name 56 | * @return void 57 | */ 58 | public function build(Validator $validator, $name = 'default') { 59 | if ($name !== $this->_config['validator']) { 60 | return; 61 | } 62 | 63 | $field = $this->_config['field']; 64 | $message = $this->_config['message']; 65 | $validator->add($field, 'notBlank', [ 66 | 'rule' => function ($value, $context) { 67 | return !empty($value); 68 | }, 69 | 'message' => $message, 70 | 'requirePresence' => true, 71 | 'allowEmpty' => false, 72 | 'last' => true, 73 | ]); 74 | $validator->requirePresence($field); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/Model/Behavior/NeighborBehavior.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | protected array $_defaultConfig = [ 18 | ]; 19 | 20 | /** 21 | * @param int $id 22 | * @param array $options 23 | * 24 | * @throws \InvalidArgumentException 25 | * @return array 26 | */ 27 | public function neighbors($id, array $options = []) { 28 | if (!$id) { 29 | throw new InvalidArgumentException("The 'id' key is required for find('neighbors')"); 30 | } 31 | 32 | /** @var string $primaryKey */ 33 | $primaryKey = $this->_table->getPrimaryKey(); 34 | $sortField = $this->_table->hasField('created') ? 'created' : $primaryKey; 35 | $defaults = [ 36 | 'sortField' => $this->_table->getAlias() . '.' . $sortField, 37 | ]; 38 | $options += $defaults; 39 | 40 | $normalDirection = (!empty($options['reverse']) ? false : true); 41 | $sortDirWord = $normalDirection ? ['ASC', 'DESC'] : ['DESC', 'ASC']; 42 | $sortDirSymb = $normalDirection ? ['>=', '<='] : ['<=', '>=']; 43 | 44 | if (empty($options['value'])) { 45 | $data = $this->_table->find('all', ...['conditions' => [$primaryKey => $id]])->first(); 46 | [$model, $sortField] = pluginSplit($options['sortField']); 47 | $options['value'] = $data[$sortField]; 48 | } 49 | 50 | $return = []; 51 | 52 | $findOptions = []; 53 | if (isset($options['contain'])) { 54 | $findOptions['contain'] = $options['contain']; 55 | } 56 | 57 | if (!empty($options['fields'])) { 58 | $findOptions['fields'] = $options['fields']; 59 | } 60 | $findOptions['conditions'][$this->_table->getAlias() . '.' . $primaryKey . ' !='] = $id; 61 | 62 | $prevOptions = $findOptions; 63 | $prevOptions['conditions'] = Hash::merge($prevOptions['conditions'], [$options['sortField'] . ' ' . $sortDirSymb[1] => $options['value']]); 64 | $prevOptions['order'] = [$options['sortField'] => $sortDirWord[1]]; 65 | $return['prev'] = $this->_table->find('all', ...$prevOptions)->first(); 66 | 67 | $nextOptions = $findOptions; 68 | $nextOptions['conditions'] = Hash::merge($nextOptions['conditions'], [$options['sortField'] . ' ' . $sortDirSymb[0] => $options['value']]); 69 | $nextOptions['order'] = [$options['sortField'] => $sortDirWord[0]]; 70 | $return['next'] = $this->_table->find('all', ...$nextOptions)->first(); 71 | 72 | return $return; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/Model/Behavior/TypeMapBehavior.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | protected array $_defaultConfig = [ 22 | 'fields' => [], // Fields to change column type for 23 | ]; 24 | 25 | /** 26 | * @param array $config 27 | * @throws \RuntimeException 28 | * @return void 29 | */ 30 | public function initialize(array $config): void { 31 | if (empty($this->_config['fields'])) { 32 | throw new RuntimeException('Fields are required'); 33 | } 34 | if (!is_array($this->_config['fields'])) { 35 | $this->_config['fields'] = (array)$this->_config['fields']; 36 | } 37 | 38 | foreach ($this->_config['fields'] as $field => $type) { 39 | if (is_array($type)) { 40 | $type = $type['type']; 41 | } 42 | if (!is_string($type)) { 43 | throw new RuntimeException('Invalid field type setup.'); 44 | } 45 | 46 | $this->_table->getSchema()->setColumnType($field, $type); 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/Model/Entity/Entity.php: -------------------------------------------------------------------------------- 1 | |string|int|null $value Integer or array of keys or NULL for complete array result 13 | * @param array $options Options 14 | * @param string|null $default Default value 15 | * @return array|string|null 16 | */ 17 | public static function enum($value, array $options, $default = null) { 18 | if ($value !== null && !is_array($value)) { 19 | if (array_key_exists($value, $options)) { 20 | return $options[$value]; 21 | } 22 | 23 | return $default; 24 | } 25 | if ($value !== null) { 26 | $newOptions = []; 27 | foreach ($value as $v) { 28 | $newOptions[$v] = $options[$v]; 29 | } 30 | 31 | return $newOptions; 32 | } 33 | 34 | return $options; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/Model/Entity/Token.php: -------------------------------------------------------------------------------- 1 | $cases Provide for narrowing or resorting. 14 | * 15 | * @return array 16 | */ 17 | public static function options(array $cases = []): array { 18 | $options = []; 19 | 20 | if ($cases) { 21 | foreach ($cases as $case) { 22 | if (!($case instanceof BackedEnum)) { 23 | $case = static::from($case); 24 | } 25 | 26 | $options[(string)$case->value] = $case->label(); 27 | } 28 | 29 | return $options; 30 | } 31 | 32 | $cases = static::cases(); 33 | foreach ($cases as $case) { 34 | $options[(string)$case->value] = $case->label(); 35 | } 36 | 37 | return $options; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/ToolsPlugin.php: -------------------------------------------------------------------------------- 1 | plugin('Tools', function (RouteBuilder $routes): void { 24 | $routes->fallbacks(); 25 | }); 26 | 27 | $routes->prefix('Admin', function (RouteBuilder $routes): void { 28 | $routes->plugin('Tools', function (RouteBuilder $routes): void { 29 | $routes->connect('/', ['controller' => 'Tools', 'action' => 'index']); 30 | 31 | $routes->fallbacks(); 32 | }); 33 | }); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/Utility/Clock.php: -------------------------------------------------------------------------------- 1 | 'File', 35 | 'path' => LOGS, 36 | 'levels' => [], 37 | 'scopes' => ['custom'], 38 | 'file' => $filename, 39 | ]); 40 | 41 | static::$_debugConfig = CoreLog::getConfig('debug'); 42 | 43 | CoreLog::drop('debug'); 44 | } 45 | 46 | /** 47 | * Log data into custom file 48 | * 49 | * @param \ArrayObject|array|string $data Data to store 50 | * @param string|null $filename Filename of log file 51 | * @param bool $traceKey Add trace string key into log data 52 | * @return bool Success 53 | */ 54 | public static function write($data, $filename = null, $traceKey = false): bool { 55 | static::_init($filename); 56 | 57 | // Pretty print array or object 58 | if (is_array($data) || is_object($data)) { 59 | if ($traceKey) { 60 | try { 61 | throw new Exception('Trace string', 1); 62 | } catch (Throwable $t) { 63 | $data['trace_string'] = $t->getTraceAsString(); 64 | } 65 | } 66 | 67 | $data = print_r($data, true); 68 | } 69 | 70 | $logged = CoreLog::write('debug', $data, ['scope' => 'custom']); 71 | 72 | static::_cleanUp(); 73 | 74 | return $logged; 75 | } 76 | 77 | /** 78 | * Drop custom log config, set default `debug` config in log registry. 79 | * 80 | * @return void 81 | */ 82 | protected static function _cleanUp(): void { 83 | CoreLog::drop('custom'); 84 | 85 | CoreLog::setConfig('debug', static::$_debugConfig); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/Utility/Number.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | protected array $_defaultConfigExt = [ 22 | 'novalidate' => false, 23 | ]; 24 | 25 | /** 26 | * Construct the widgets and binds the default context providers 27 | * 28 | * @param \Cake\View\View $View The View this helper is being attached to. 29 | * @param array $config Configuration settings for the helper. 30 | */ 31 | public function __construct(View $View, array $config = []) { 32 | $this->_defaultConfig += $this->_defaultConfigExt; 33 | $defaultConfig = (array)Configure::read('FormConfig'); 34 | if ($defaultConfig) { 35 | $this->_defaultConfig = Hash::merge($this->_defaultConfig, $defaultConfig); 36 | } 37 | parent::__construct($View, $config); 38 | } 39 | 40 | /** 41 | * Overwrite to allow FormConfig Configure settings to be applied. 42 | * 43 | * @param mixed $context The context for which the form is being defined. Can 44 | * be an ORM entity, ORM resultset, or an array of meta data. You can use false or null 45 | * to make a model-less form. 46 | * @param array $options An array of html attributes and options. 47 | * @return string An formatted opening FORM tag. 48 | */ 49 | public function create(mixed $context = null, array $options = []): string { 50 | $defaults = ['novalidate' => $this->_defaultConfig['novalidate']]; 51 | $options += $defaults; 52 | 53 | return parent::create($context, $options); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/View/Helper/HtmlHelper.php: -------------------------------------------------------------------------------- 1 | [], 30 | 'paths' => [], 31 | 'autoPrefix' => true, // For primary set no prefix is required 32 | 'separator' => ':', 33 | ]; 34 | 35 | /** 36 | * @param \Cake\View\View $View 37 | * @param array $config 38 | */ 39 | public function __construct(View $View, array $config = []) { 40 | $defaults = (array)Configure::read('Icon') + $this->_defaults; 41 | $config += $defaults; 42 | 43 | $this->collection = new IconCollection($config); 44 | 45 | parent::__construct($View, $config); 46 | } 47 | 48 | /** 49 | * @return array> 50 | */ 51 | public function names(): array { 52 | return $this->collection->names(); 53 | } 54 | 55 | /** 56 | * Icons using the default namespace or an already prefixed one. 57 | * 58 | * @param string $icon (constant or filename) 59 | * @param array $options : 60 | * - translate, title, ... 61 | * @param array $attributes : 62 | * - class, ... 63 | * @return string 64 | */ 65 | public function render(string $icon, array $options = [], array $attributes = []): string { 66 | return $this->collection->render($icon, $options, $attributes); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/View/Helper/NumberHelper.php: -------------------------------------------------------------------------------- 1 | $config Configuration settings for the helper 30 | */ 31 | public function __construct(View $view, array $config = []) { 32 | $config += ['engine' => 'Tools.Number']; 33 | 34 | parent::__construct($view, $config); 35 | 36 | /** @psalm-var class-string<\Cake\I18n\Number>|null $engineClass */ 37 | $engineClass = App::className($config['engine'], 'I18n'); 38 | if ($engineClass === null) { 39 | throw new CakeException(sprintf('Class for `%s` could not be found', $config['engine'])); 40 | } 41 | 42 | /** @phpstan-ignore-next-line */ 43 | $this->_engine = new $engineClass($config); 44 | } 45 | 46 | /** 47 | * Call methods from Cake\I18n\Number utility class 48 | * 49 | * @param string $method Method to invoke 50 | * @param array $params Array of params for the method. 51 | * @return mixed Whatever is returned by called method, or false on failure 52 | */ 53 | public function __call(string $method, array $params): mixed { 54 | return $this->_engine->{$method}(...$params); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/View/Helper/UrlHelper.php: -------------------------------------------------------------------------------- 1 | defaults(); 13 | 14 | return $url; 15 | } 16 | 17 | /** 18 | * @param array $url 19 | * @return array 20 | */ 21 | public function completeArray(array $url): array { 22 | $url = $this->addQueryStrings($url); 23 | $url = $this->addPassed($url); 24 | 25 | return $url; 26 | } 27 | 28 | /** 29 | * Creates a reset URL. 30 | * The prefix and plugin params are resetting to default false. 31 | * 32 | * Can only add defaults for array URLs. 33 | * 34 | * @param array|string|null $url URL. 35 | * @param array $options 36 | * @return string Full translated URL with base path. 37 | */ 38 | public function buildReset($url, array $options = []): string { 39 | if (is_array($url)) { 40 | $url += $this->defaults(); 41 | } 42 | 43 | return $this->build($url, $options); 44 | } 45 | 46 | /** 47 | * Returns a URL based on provided parameters. 48 | * 49 | * Can only add query strings for array URLs. 50 | * 51 | * @param array|string|null $url URL. 52 | * @param array $options 53 | * @return string Full translated URL with base path. 54 | */ 55 | public function buildComplete($url, array $options = []): string { 56 | if (is_array($url)) { 57 | $url = $this->addQueryStrings($url); 58 | } 59 | 60 | return $this->build($url, $options); 61 | } 62 | 63 | /** 64 | * @return array 65 | */ 66 | public function defaults(): array { 67 | return [ 68 | 'prefix' => false, 69 | 'plugin' => false, 70 | ]; 71 | } 72 | 73 | /** 74 | * @param array $url 75 | * 76 | * @return array 77 | */ 78 | protected function addQueryStrings(array $url): array { 79 | if (!isset($url['?'])) { 80 | $url['?'] = []; 81 | } 82 | $url['?'] += $this->_View->getRequest()->getQuery(); 83 | 84 | return $url; 85 | } 86 | 87 | /** 88 | * @param array $url 89 | * 90 | * @return array 91 | */ 92 | protected function addPassed(array $url): array { 93 | $pass = $this->_View->getRequest()->getParam('pass'); 94 | $url = array_merge($url, $pass); 95 | 96 | return $url; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/View/Icon/AbstractIcon.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | protected $config; 19 | 20 | /** 21 | * @param array $config 22 | */ 23 | public function __construct(array $config = []) { 24 | $this->template = new StringTemplate(['icon' => $config['template']]); 25 | $this->config = $config; 26 | } 27 | 28 | /** 29 | * @return string 30 | */ 31 | protected function path(): string { 32 | $path = $this->config['path'] ?? null; 33 | if (!$path) { 34 | throw new RuntimeException('You need to define a meta data file path for `' . static::class . '` in order to get icon names.'); 35 | } 36 | if (!file_exists($path)) { 37 | throw new RuntimeException('Cannot find meta data file path `' . $path . '` for `' . static::class . '`.'); 38 | } 39 | 40 | return $path; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/View/Icon/BootstrapIcon.php: -------------------------------------------------------------------------------- 1 | $config 14 | */ 15 | public function __construct(array $config = []) { 16 | $config += [ 17 | 'template' => '', 18 | ]; 19 | 20 | parent::__construct($config); 21 | } 22 | 23 | /** 24 | * @return array 25 | */ 26 | public function names(): array { 27 | $path = $this->path(); 28 | 29 | return BootstrapIconCollector::collect($path); 30 | } 31 | 32 | /** 33 | * @param string $icon 34 | * @param array $options 35 | * @param array $attributes 36 | * 37 | * @return string 38 | */ 39 | public function render(string $icon, array $options = [], array $attributes = []): string { 40 | if (!empty($this->config['attributes'])) { 41 | $attributes += $this->config['attributes']; 42 | } 43 | 44 | $options['class'] = 'bi bi-' . $icon; 45 | if (!empty($attributes['class'])) { 46 | $options['class'] .= ' ' . $attributes['class']; 47 | } 48 | $options['attributes'] = $this->template->formatAttributes($attributes, ['class']); 49 | 50 | return $this->template->format('icon', $options); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/View/Icon/Collector/BootstrapIconCollector.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public static function collect(string $filePath): array { 18 | $content = file_get_contents($filePath); 19 | if ($content === false) { 20 | throw new RuntimeException('Cannot read file: ' . $filePath); 21 | } 22 | $array = json_decode($content, true); 23 | if (!$array) { 24 | throw new RuntimeException('Cannot parse JSON: ' . $filePath); 25 | } 26 | 27 | /** @var array */ 28 | return array_keys($array); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/View/Icon/Collector/FeatherIconCollector.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public static function collect(string $filePath): array { 18 | $content = file_get_contents($filePath); 19 | if ($content === false) { 20 | throw new RuntimeException('Cannot read file: ' . $filePath); 21 | } 22 | $array = json_decode($content, true); 23 | if (!$array) { 24 | throw new RuntimeException('Cannot parse JSON: ' . $filePath); 25 | } 26 | 27 | /** @var array */ 28 | return array_keys($array); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/View/Icon/Collector/FontAwesome4IconCollector.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public static function collect(string $filePath): array { 18 | $content = file_get_contents($filePath); 19 | if ($content === false) { 20 | throw new RuntimeException('Cannot read file: ' . $filePath); 21 | } 22 | 23 | $ext = pathinfo($filePath, PATHINFO_EXTENSION); 24 | switch ($ext) { 25 | case 'less': 26 | preg_match_all('/@fa-var-([0-9a-z-]+):/', $content, $matches); 27 | 28 | break; 29 | case 'scss': 30 | preg_match_all('/\$fa-var-([0-9a-z-]+):/', $content, $matches); 31 | 32 | break; 33 | default: 34 | throw new RuntimeException('Format not supported: ' . $ext); 35 | } 36 | 37 | $icons = !empty($matches[1]) ? $matches[1] : []; 38 | 39 | return $icons; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/View/Icon/Collector/FontAwesome5IconCollector.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public static function collect(string $filePath): array { 18 | $content = file_get_contents($filePath); 19 | if ($content === false) { 20 | throw new RuntimeException('Cannot read file: ' . $filePath); 21 | } 22 | 23 | $ext = pathinfo($filePath, PATHINFO_EXTENSION); 24 | switch ($ext) { 25 | case 'svg': 26 | preg_match_all('/symbol id="([a-z][^"]+)"/', $content, $matches); 27 | if (empty($matches[1])) { 28 | throw new RuntimeException('Cannot parse SVG: ' . $filePath); 29 | } 30 | $icons = $matches[1]; 31 | 32 | break; 33 | case 'yml': 34 | $array = yaml_parse($content); 35 | /** @var array $icons */ 36 | $icons = array_keys($array); 37 | 38 | break; 39 | case 'json': 40 | $array = json_decode($content, true); 41 | if (!$array) { 42 | throw new RuntimeException('Cannot parse JSON: ' . $filePath); 43 | } 44 | 45 | $icons = []; 46 | foreach ($array['icons'] as $row) { 47 | $icons[] = $row['name']; 48 | } 49 | 50 | break; 51 | default: 52 | throw new RuntimeException('Unknown file extension: ' . $ext); 53 | } 54 | 55 | return $icons; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/View/Icon/Collector/FontAwesome6IconCollector.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public static function collect(string $filePath): array { 18 | $content = file_get_contents($filePath); 19 | if ($content === false) { 20 | throw new RuntimeException('Cannot read file: ' . $filePath); 21 | } 22 | 23 | $array = json_decode($content, true); 24 | if (!$array) { 25 | throw new RuntimeException('Cannot parse JSON: ' . $filePath); 26 | } 27 | /** @var array $icons */ 28 | $icons = array_keys($array); 29 | 30 | return $icons; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/View/Icon/Collector/MaterialIconCollector.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public static function collect(string $filePath): array { 18 | $content = file_get_contents($filePath); 19 | if ($content === false) { 20 | throw new RuntimeException('Cannot read file: ' . $filePath); 21 | } 22 | 23 | preg_match_all('/"(.+)"/u', $content, $matches); 24 | if (empty($matches[1])) { 25 | throw new RuntimeException('Cannot parse content: ' . $filePath); 26 | } 27 | 28 | /** @var array */ 29 | return $matches[1]; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/View/Icon/FeatherIcon.php: -------------------------------------------------------------------------------- 1 | $config 14 | */ 15 | public function __construct(array $config = []) { 16 | $config += [ 17 | 'template' => '', 18 | ]; 19 | 20 | parent::__construct($config); 21 | } 22 | 23 | /** 24 | * @return array 25 | */ 26 | public function names(): array { 27 | $path = $this->path(); 28 | 29 | return FeatherIconCollector::collect($path); 30 | } 31 | 32 | /** 33 | * @param string $icon 34 | * @param array $options 35 | * @param array $attributes 36 | * 37 | * @return string 38 | */ 39 | public function render(string $icon, array $options = [], array $attributes = []): string { 40 | if (!empty($this->config['attributes'])) { 41 | $attributes += $this->config['attributes']; 42 | } 43 | 44 | $options['name'] = $icon; 45 | $options['attributes'] = $this->template->formatAttributes($attributes); 46 | 47 | return $this->template->format('icon', $options); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/View/Icon/FontAwesome4Icon.php: -------------------------------------------------------------------------------- 1 | $config 14 | */ 15 | public function __construct(array $config = []) { 16 | $config += [ 17 | 'template' => '', 18 | ]; 19 | 20 | parent::__construct($config); 21 | } 22 | 23 | /** 24 | * @return array 25 | */ 26 | public function names(): array { 27 | $path = $this->path(); 28 | 29 | return FontAwesome4IconCollector::collect($path); 30 | } 31 | 32 | /** 33 | * @param string $icon 34 | * @param array $options 35 | * @param array $attributes 36 | * 37 | * @return string 38 | */ 39 | public function render(string $icon, array $options = [], array $attributes = []): string { 40 | if (!empty($this->config['attributes'])) { 41 | $attributes += $this->config['attributes']; 42 | } 43 | 44 | // Shimming 45 | if (isset($options['title'])) { 46 | $attributes['title'] = $options['title']; 47 | unset($options['title']); 48 | } 49 | 50 | $namespace = 'fa'; 51 | 52 | $class = [ 53 | $namespace, 54 | ]; 55 | if (!empty($options['extra'])) { 56 | foreach ($options['extra'] as $i) { 57 | $class[] = $namespace . '-' . $i; 58 | } 59 | } 60 | if (!empty($options['size'])) { 61 | $class[] = $namespace . '-' . ($options['size'] === 'large' ? 'large' : $options['size'] . 'x'); 62 | } 63 | if (!empty($options['pull'])) { 64 | $class[] = 'pull-' . $options['pull']; 65 | } 66 | if (!empty($options['rotate'])) { 67 | $class[] = $namespace . '-rotate-' . (int)$options['rotate']; 68 | } 69 | if (!empty($options['spin'])) { 70 | $class[] = $namespace . '-spin'; 71 | } 72 | 73 | $options['class'] = implode(' ', $class) . ' ' . $namespace . '-' . $icon; 74 | if (!empty($attributes['class'])) { 75 | $options['class'] .= ' ' . $attributes['class']; 76 | } 77 | $options['attributes'] = $this->template->formatAttributes($attributes, ['class']); 78 | 79 | return $this->template->format('icon', $options); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/View/Icon/FontAwesome5Icon.php: -------------------------------------------------------------------------------- 1 | $config 14 | */ 15 | public function __construct(array $config = []) { 16 | $config += [ 17 | 'template' => '', 18 | 'namespace' => 'fas', 19 | ]; 20 | 21 | parent::__construct($config); 22 | } 23 | 24 | /** 25 | * @return array 26 | */ 27 | public function names(): array { 28 | $path = $this->path(); 29 | 30 | return FontAwesome5IconCollector::collect($path); 31 | } 32 | 33 | /** 34 | * @param string $icon 35 | * @param array $options 36 | * @param array $attributes 37 | * 38 | * @return string 39 | */ 40 | public function render(string $icon, array $options = [], array $attributes = []): string { 41 | if (!empty($this->config['attributes'])) { 42 | $attributes += $this->config['attributes']; 43 | } 44 | 45 | // Shimming 46 | if (isset($options['title'])) { 47 | $attributes['title'] = $options['title']; 48 | unset($options['title']); 49 | } 50 | 51 | $class = [ 52 | $this->config['namespace'], 53 | ]; 54 | if (!empty($options['extra'])) { 55 | foreach ($options['extra'] as $i) { 56 | $class[] = 'fa-' . $i; 57 | } 58 | } 59 | if (!empty($options['size'])) { 60 | $class[] = 'fa-' . ($options['size'] === 'large' ? 'large' : $options['size'] . 'x'); 61 | } 62 | if (!empty($options['pull'])) { 63 | $class[] = 'pull-' . $options['pull']; 64 | } 65 | if (!empty($options['rotate'])) { 66 | $class[] = 'fa-rotate-' . (int)$options['rotate']; 67 | } 68 | if (!empty($options['spin'])) { 69 | $class[] = 'fa-spin'; 70 | } 71 | 72 | $options['class'] = implode(' ', $class) . ' ' . 'fa-' . $icon; 73 | if (!empty($attributes['class'])) { 74 | $options['class'] .= ' ' . $attributes['class']; 75 | } 76 | $options['attributes'] = $this->template->formatAttributes($attributes, ['class']); 77 | 78 | return $this->template->format('icon', $options); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/View/Icon/FontAwesome6Icon.php: -------------------------------------------------------------------------------- 1 | $config 14 | */ 15 | public function __construct(array $config = []) { 16 | $config += [ 17 | 'template' => '', 18 | 'namespace' => 'solid', 19 | ]; 20 | 21 | parent::__construct($config); 22 | } 23 | 24 | /** 25 | * @return array 26 | */ 27 | public function names(): array { 28 | $path = $this->path(); 29 | 30 | return FontAwesome6IconCollector::collect($path); 31 | } 32 | 33 | /** 34 | * @param string $icon 35 | * @param array $options 36 | * @param array $attributes 37 | * 38 | * @return string 39 | */ 40 | public function render(string $icon, array $options = [], array $attributes = []): string { 41 | if (!empty($this->config['attributes'])) { 42 | $attributes += $this->config['attributes']; 43 | } 44 | 45 | // Shimming 46 | if (isset($options['title'])) { 47 | $attributes['title'] = $options['title']; 48 | unset($options['title']); 49 | } 50 | 51 | $namespace = 'fa-' . $this->config['namespace']; 52 | 53 | $class = [ 54 | $namespace, 55 | ]; 56 | if (!empty($options['extra'])) { 57 | foreach ($options['extra'] as $i) { 58 | $class[] = 'fa-' . $i; 59 | } 60 | } 61 | if (!empty($options['size'])) { 62 | $class[] = 'fa-' . ($options['size'] === 'large' ? 'large' : $options['size'] . 'x'); 63 | } 64 | if (!empty($options['pull'])) { 65 | $class[] = 'pull-' . $options['pull']; 66 | } 67 | if (!empty($options['rotate'])) { 68 | $class[] = 'fa-rotate-' . (int)$options['rotate']; 69 | } 70 | if (!empty($options['spin'])) { 71 | $class[] = 'fa-spin'; 72 | } 73 | 74 | $options['class'] = implode(' ', $class) . ' ' . 'fa-' . $icon; 75 | if (!empty($attributes['class'])) { 76 | $options['class'] .= ' ' . $attributes['class']; 77 | } 78 | $options['attributes'] = $this->template->formatAttributes($attributes, ['class']); 79 | 80 | return $this->template->format('icon', $options); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/View/Icon/IconInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | public function names(): array; 11 | 12 | /** 13 | * Icon formatting using the specific engine. 14 | * 15 | * @param string $icon Icon name 16 | * @param array $options : 17 | * - translate, title, ... 18 | * @param array $attributes : 19 | * - class, ... 20 | * @return string 21 | */ 22 | public function render(string $icon, array $options = [], array $attributes = []): string; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/View/Icon/MaterialIcon.php: -------------------------------------------------------------------------------- 1 | $config 14 | */ 15 | public function __construct(array $config = []) { 16 | $config += [ 17 | 'template' => '{{name}}', 18 | 'namespace' => 'material-icons', 19 | ]; 20 | 21 | parent::__construct($config); 22 | } 23 | 24 | /** 25 | * @return array 26 | */ 27 | public function names(): array { 28 | $path = $this->path(); 29 | 30 | return MaterialIconCollector::collect($path); 31 | } 32 | 33 | /** 34 | * @param string $icon 35 | * @param array $options 36 | * @param array $attributes 37 | * 38 | * @return string 39 | */ 40 | public function render(string $icon, array $options = [], array $attributes = []): string { 41 | if (!empty($this->config['attributes'])) { 42 | $attributes += $this->config['attributes']; 43 | } 44 | 45 | $options['name'] = $icon; 46 | $options['class'] = $this->config['namespace']; 47 | if (!empty($attributes['class'])) { 48 | $options['class'] .= ' ' . $attributes['class']; 49 | } 50 | $options['attributes'] = $this->template->formatAttributes($attributes, ['class']); 51 | 52 | return $this->template->format('icon', $options); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/View/Widget/DatalistWidget.php: -------------------------------------------------------------------------------- 1 | null, 28 | 'name' => '', 29 | 'empty' => false, 30 | 'escape' => true, 31 | 'options' => [], 32 | 'disabled' => null, 33 | 'val' => null, 34 | 'input' => [], 35 | 'keys' => false, 36 | 'templateVars' => [], 37 | ]; 38 | 39 | $options = $this->_renderContent($data); 40 | if (!$data['keys']) { 41 | $options = str_replace( 42 | 'value', 43 | 'data-value', 44 | $options, 45 | ); 46 | } 47 | 48 | $name = $data['name']; 49 | $id = $data['id'] ?: Text::slug($name); 50 | $default = $data['val'] ?? null; 51 | 52 | $inputData = $data['input'] + [ 53 | 'id' => $id, 54 | 'name' => $name, 55 | 'autocomplete' => 'off', 56 | ]; 57 | 58 | unset($data['name'], $data['options'], $data['empty'], $data['val'], $data['escape'], $data['keys'], $data['input'], $data['id']); 59 | if (isset($data['disabled']) && is_array($data['disabled'])) { 60 | unset($data['disabled']); 61 | } 62 | 63 | $inputData['value'] = $default; 64 | $inputAttrs = $this->_templates->formatAttributes($inputData); 65 | 66 | $datalistAttrs = $this->_templates->formatAttributes($data); 67 | 68 | return $this->_templates->format( 69 | 'datalist', 70 | [ 71 | 'name' => $name, 72 | 'inputAttrs' => $inputAttrs, 73 | 'datalistAttrs' => $datalistAttrs, 74 | 'content' => implode('', $options), 75 | 'id' => $id, 76 | ], 77 | ); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /templates/Admin/Helper/bitmasks.php: -------------------------------------------------------------------------------- 1 | 6 |

Bitmasks

7 | Using the BitmaskedBehavior 8 | 9 |

Re-configure using SQL-Snippets

10 | Syntax: OLDID[,...]:NEWID[,...] (allowing multiple ids on each side
11 | e.g.: 4:8,16, single statements per line 12 | 13 |
14 | Form->create();?> 15 |
16 | 17 | Form->control('model', ['placeholder' => 'PluginName.ModelName']); 19 | echo $this->Form->control('field', ['placeholder' => 'field_name']); 20 | echo $this->Form->control('matrix', ['type' => 'textarea']); 21 | ?> 22 |
23 | Form->submit(__('Submit')); echo $this->Form->end();?> 24 |
25 | 26 | 27 |

Result

28 | $value) { 30 | echo pre($value['from']); 31 | echo pre($value['to']); 32 | 33 | } 34 | echo '
';
35 | foreach ($result as $key => $value) {
36 | 	echo $value['sql'] . PHP_EOL;
37 | }
38 | echo '
'; 39 | ?> 40 | 41 | -------------------------------------------------------------------------------- /templates/Admin/Helper/chars.php: -------------------------------------------------------------------------------- 1 | 8 |

String analyzer

9 | 10 |

Explain any input string

11 | 12 |
13 | Form->create();?> 14 |
15 | 16 | Form->control('string', ['type' => 'textarea']); 18 | ?> 19 |
20 | Form->submit(__('Submit')); echo $this->Form->end();?> 21 |
22 | 23 | 24 |

Result

25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | $row) { 39 | ?> 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
#CharacterUnicodeNameType
52 | -------------------------------------------------------------------------------- /templates/Admin/Pages/index.php: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |

Pages

9 | 10 |
    11 |
  • 14 | Html->linkReset($page['label'], ['controller' => 'Pages', 'action' => 'display', $page['action']]); 16 | ?> 17 |
  • 18 | 19 |
20 | 21 |
22 | -------------------------------------------------------------------------------- /templates/Admin/Tools/index.php: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |
9 |

Tools plugin

10 | 11 |

Useful quick-tools

12 |
    13 |
  • Html->link('Available "pages"', ['controller' => 'Pages', 'action' => 'index']); ?>
  • 14 |
  • Html->link('String analyzer (explain input string)', ['controller' => 'Helper', 'action' => 'chars']);?>
  • 15 |
  • Html->link('Bitmasks', ['controller' => 'Helper', 'action' => 'bitmasks']);?>
  • 16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /templates/element/pagination.php: -------------------------------------------------------------------------------- 1 | 47 | 48 |
49 |
50 | 51 |
    52 | Paginator->first($first, ['escape' => $escape]);?> 53 | 54 | Paginator->prev($prev, ['escape' => $escape, 'disabledTitle' => false]);?> 55 | 56 | Paginator->numbers(['escape' => $escape, 'separator' => $separator, 'modulus' => $modulus]);?> 57 | 58 | Paginator->next($next, ['escape' => $escape, 'disabledTitle' => false]);?> 59 | 60 | Paginator->last($last, ['escape' => $escape]);?> 61 |
62 | 63 |
64 |
65 |

66 | Paginator->counter($format); ?> 67 |

68 |
69 |
70 | Js)) { 90 | $this->Js->buffer($script); 91 | } 92 | } ?> 93 | -------------------------------------------------------------------------------- /tests/Fixture/AfterTreesFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer'], 19 | 'parent_id' => ['type' => 'integer'], 20 | 'lft' => ['type' => 'integer'], 21 | 'rght' => ['type' => 'integer'], 22 | 'name' => ['type' => 'string', 'length' => 255, 'null' => false], 23 | '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], 24 | ]; 25 | 26 | /** 27 | * records property 28 | * 29 | * @var array 30 | */ 31 | public array $records = [ 32 | ['parent_id' => null, 'lft' => 1, 'rght' => 2, 'name' => 'One'], 33 | ['parent_id' => null, 'lft' => 3, 'rght' => 4, 'name' => 'Two'], 34 | ['parent_id' => null, 'lft' => 5, 'rght' => 6, 'name' => 'Three'], 35 | ['parent_id' => null, 'lft' => 7, 'rght' => 12, 'name' => 'Four'], 36 | ['parent_id' => null, 'lft' => 8, 'rght' => 9, 'name' => 'Five'], 37 | ['parent_id' => null, 'lft' => 10, 'rght' => 11, 'name' => 'Six'], 38 | ['parent_id' => null, 'lft' => 13, 'rght' => 14, 'name' => 'Seven'], 39 | ]; 40 | 41 | } 42 | -------------------------------------------------------------------------------- /tests/Fixture/ArticlesFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer'], 19 | 'author_id' => ['type' => 'integer', 'null' => true], 20 | 'title' => ['type' => 'string', 'null' => true], 21 | 'body' => 'text', 22 | 'published' => ['type' => 'string', 'length' => 1, 'default' => 'N'], 23 | '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], 24 | ]; 25 | 26 | /** 27 | * records property 28 | * 29 | * @var array 30 | */ 31 | public array $records = [ 32 | ['author_id' => 1, 'title' => 'First Article', 'body' => 'First Article Body', 'published' => 'Y'], 33 | ['author_id' => 3, 'title' => 'Second Article', 'body' => 'Second Article Body', 'published' => 'Y'], 34 | ['author_id' => 1, 'title' => 'Third Article', 'body' => 'Third Article Body', 'published' => 'Y'], 35 | ]; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /tests/Fixture/AuthorsFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer'], 19 | 'name' => ['type' => 'string', 'default' => null], 20 | '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], 21 | ]; 22 | 23 | /** 24 | * records property 25 | * 26 | * @var array 27 | */ 28 | public array $records = [ 29 | ['name' => 'mariano'], 30 | ['name' => 'nate'], 31 | ['name' => 'larry'], 32 | ['name' => 'garrett'], 33 | ]; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /tests/Fixture/BitmaskedCommentsFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer'], 20 | 'article_id' => ['type' => 'integer', 'null' => true], 21 | 'user_id' => ['type' => 'integer', 'null' => true], 22 | 'comment' => 'text', 23 | 'status' => ['type' => 'integer', 'null' => false, 'length' => 2, 'default' => 0], 24 | 'created' => 'datetime', 25 | 'updated' => 'datetime', 26 | '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], 27 | ]; 28 | 29 | /** 30 | * Records property 31 | * 32 | * @var array 33 | */ 34 | public array $records = [ 35 | ['article_id' => 1, 'user_id' => 2, 'comment' => 'First Comment for First Article', 'status' => 0, 'created' => '2007-03-18 10:45:23', 'updated' => '2007-03-18 10:47:31'], 36 | ['article_id' => 1, 'user_id' => 4, 'comment' => 'Second Comment for First Article', 'status' => BitmaskedComment::STATUS_ACTIVE, 'created' => '2007-03-18 10:47:23', 'updated' => '2007-03-18 10:49:31'], 37 | ['article_id' => 1, 'user_id' => 1, 'comment' => 'Third Comment for First Article', 'status' => BitmaskedComment::STATUS_PUBLISHED, 'created' => '2007-03-18 10:49:23', 'updated' => '2007-03-18 10:51:31'], 38 | ['article_id' => 1, 'user_id' => 1, 'comment' => 'Fourth Comment for First Article', 'status' => 3, 'created' => '2007-03-18 10:51:23', 'updated' => '2007-03-18 10:53:31'], 39 | ['article_id' => 2, 'user_id' => 1, 'comment' => 'First Comment for Second Article', 'status' => BitmaskedComment::STATUS_APPROVED, 'created' => '2007-03-18 10:53:23', 'updated' => '2007-03-18 10:55:31'], 40 | ['article_id' => 2, 'user_id' => 2, 'comment' => 'Second Comment for Second Article', 'status' => 5, 'created' => '2007-03-18 10:55:23', 'updated' => '2007-03-18 10:57:31'], 41 | ['article_id' => 2, 'user_id' => 3, 'comment' => 'Comment With All Bits set', 'status' => 15, 'created' => '2007-03-18 10:55:23', 'updated' => '2007-03-18 10:57:31'], 42 | ]; 43 | 44 | } 45 | -------------------------------------------------------------------------------- /tests/Fixture/DataFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer', 'length' => 11, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null], 16 | 'name' => ['type' => 'string', 'length' => 50, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], 17 | 'data_json' => ['type' => 'text', 'length' => 16777215, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], 18 | 'data_array' => ['type' => 'text', 'length' => 16777215, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], 19 | '_constraints' => [ 20 | 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], 21 | ], 22 | ]; 23 | 24 | /** 25 | * Records 26 | * 27 | * @var array 28 | */ 29 | public array $records = [ 30 | [ 31 | 'name' => 'Lorem ipsum dolor sit amet', 32 | 'data_json' => null, 33 | 'data_array' => null, 34 | ], 35 | ]; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /tests/Fixture/JsonableCommentsFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer'], 14 | 'comment' => ['type' => 'string', 'length' => 255, 'null' => false], 15 | 'url' => ['type' => 'string', 'length' => 255, 'null' => false], 16 | 'title' => ['type' => 'string', 'length' => 255, 'null' => false], 17 | 'details' => ['type' => 'string', 'length' => 255, 'null' => false], 18 | 'details_nullable' => ['type' => 'string', 'length' => 255, 'null' => true], 19 | '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], 20 | ]; 21 | 22 | /** 23 | * @var array 24 | */ 25 | public array $records = [ 26 | ]; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /tests/Fixture/MultiColumnUsersFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer'], 16 | 'user_name' => ['type' => 'string', 'null' => false], 17 | 'email' => ['type' => 'string', 'null' => false], 18 | 'password' => ['type' => 'string', 'null' => false], 19 | 'token' => ['type' => 'string', 'null' => false], 20 | 'created' => 'datetime', 21 | 'updated' => 'datetime', 22 | '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], 23 | ]; 24 | 25 | /** 26 | * records property 27 | * 28 | * @var array 29 | */ 30 | public array $records = [ 31 | [ 32 | 'user_name' => 'mariano', 33 | 'email' => 'mariano@example.com', 34 | 'password' => '5f4dcc3b5aa765d61d8327deb882cf99', 35 | 'token' => '12345', 'created' => '2007-03-17 01:16:23', 36 | 'updated' => '2007-03-17 01:18:31', 37 | ], 38 | [ 39 | 'user_name' => 'nate', 40 | 'email' => 'nate@example.com', 41 | 'password' => '5f4dcc3b5aa765d61d8327deb882cf99', 42 | 'token' => '23456', 43 | 'created' => '2007-03-17 01:18:23', 44 | 'updated' => '2007-03-17 01:20:31', 45 | ], 46 | [ 47 | 'user_name' => 'larry', 48 | 'email' => 'larry@example.com', 49 | 'password' => '5f4dcc3b5aa765d61d8327deb882cf99', 50 | 'token' => '34567', 51 | 'created' => '2007-03-17 01:20:23', 52 | 'updated' => '2007-03-17 01:22:31', 53 | ], 54 | [ 55 | 'user_name' => 'garrett', 56 | 'email' => 'garrett@example.com', 57 | 'password' => '5f4dcc3b5aa765d61d8327deb882cf99', 58 | 'token' => '45678', 59 | 'created' => '2007-03-17 01:22:23', 60 | 'updated' => '2007-03-17 01:24:31', 61 | ], 62 | [ 63 | 'user_name' => 'chartjes', 64 | 'email' => 'chartjes@example.com', 65 | 'password' => '5f4dcc3b5aa765d61d8327deb882cf99', 66 | 'token' => '56789', 67 | 'created' => '2007-03-17 01:22:23', 68 | 'updated' => '2007-03-17 01:24:31', 69 | ], 70 | ]; 71 | 72 | } 73 | -------------------------------------------------------------------------------- /tests/Fixture/PostsFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer'], 19 | 'author_id' => ['type' => 'integer', 'null' => false], 20 | 'title' => ['type' => 'string', 'null' => false], 21 | 'body' => 'text', 22 | 'published' => ['type' => 'string', 'length' => 1, 'default' => 'N'], 23 | '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], 24 | ]; 25 | 26 | /** 27 | * records property 28 | * 29 | * @var array 30 | */ 31 | public array $records = [ 32 | ['author_id' => 1, 'title' => 'First Post', 'body' => 'First Post Body', 'published' => 'Y'], 33 | ['author_id' => 3, 'title' => 'Second Post', 'body' => 'Second Post Body', 'published' => 'Y'], 34 | ['author_id' => 1, 'title' => 'Third Post', 'body' => 'Third Post Body', 'published' => 'Y'], 35 | ]; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /tests/Fixture/ResetCommentsFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer'], 19 | 'article_id' => ['type' => 'integer', 'null' => false], 20 | 'user_id' => ['type' => 'integer', 'null' => false], 21 | 'comment' => 'text', 22 | 'published' => ['type' => 'string', 'length' => 1, 'default' => 'N'], 23 | 'created' => 'datetime', 24 | 'updated' => 'datetime', 25 | '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], 26 | ]; 27 | 28 | /** 29 | * records property 30 | * 31 | * @var array 32 | */ 33 | public array $records = [ 34 | ['article_id' => 1, 'user_id' => 2, 'comment' => 'First Comment for First Article', 'published' => 'Y', 'created' => '2007-03-18 10:45:23', 'updated' => '2007-03-18 10:47:31'], 35 | ['article_id' => 1, 'user_id' => 4, 'comment' => 'Second Comment for First Article', 'published' => 'Y', 'created' => '2007-03-18 10:47:23', 'updated' => '2007-03-18 10:49:31'], 36 | ['article_id' => 1, 'user_id' => 1, 'comment' => 'Third Comment for First Article', 'published' => 'Y', 'created' => '2007-03-18 10:49:23', 'updated' => '2007-03-18 10:51:31'], 37 | ['article_id' => 1, 'user_id' => 1, 'comment' => 'Fourth Comment for First Article', 'published' => 'N', 'created' => '2007-03-18 10:51:23', 'updated' => '2007-03-18 10:53:31'], 38 | ['article_id' => 2, 'user_id' => 1, 'comment' => 'First Comment for Second Article', 'published' => 'Y', 'created' => '2007-03-18 10:53:23', 'updated' => '2007-03-18 10:55:31'], 39 | ['article_id' => 2, 'user_id' => 2, 'comment' => 'Second Comment for Second Article', 'published' => 'Y', 'created' => '2007-03-18 10:55:23', 'updated' => '2007-03-18 10:57:31'], 40 | ]; 41 | 42 | } 43 | -------------------------------------------------------------------------------- /tests/Fixture/RolesFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer'], 19 | 'name' => ['type' => 'string', 'null' => false, 'length' => 64, 'comment' => ''], 20 | 'alias' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 20, 'comment' => ''], 21 | 'default_role' => ['type' => 'boolean', 'null' => false, 'default' => false, 'collate' => null, 'comment' => 'set at register'], 22 | 'created' => ['type' => 'datetime', 'null' => true, 'default' => null, 'collate' => null, 'comment' => ''], 23 | 'modified' => ['type' => 'datetime', 'null' => true, 'default' => null, 'collate' => null, 'comment' => ''], 24 | 'sort' => ['type' => 'integer', 'null' => false, 'default' => '0', 'length' => 10, 'collate' => null, 'comment' => ''], 25 | 'active' => ['type' => 'boolean', 'null' => false, 'default' => false, 'collate' => null, 'comment' => ''], 26 | '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], 27 | ]; 28 | 29 | /** 30 | * Records 31 | * 32 | * @var array 33 | */ 34 | public array $records = [ 35 | [ 36 | 'name' => 'Super-Admin', 37 | 'alias' => 'superadmin', 38 | 'default_role' => 0, 39 | 'created' => '2010-01-07 03:36:33', 40 | 'modified' => '2010-01-07 03:36:33', 41 | 'sort' => '7', 42 | 'active' => 1, 43 | ], 44 | [ 45 | 'name' => 'Admin', 46 | 'alias' => 'admin', 47 | 'default_role' => 0, 48 | 'created' => '2010-01-07 03:36:33', 49 | 'modified' => '2010-01-07 03:36:33', 50 | 'sort' => '6', 51 | 'active' => 1, 52 | ], 53 | [ 54 | 'name' => 'User', 55 | 'alias' => 'user', 56 | 'default_role' => 1, 57 | 'created' => '2010-01-07 03:36:33', 58 | 'modified' => '2010-01-07 03:36:33', 59 | 'sort' => '1', 60 | 'active' => 1, 61 | ], 62 | [ 63 | 'name' => 'Partner', 64 | 'alias' => 'partner', 65 | 'default_role' => 0, 66 | 'created' => '2010-01-07 03:36:33', 67 | 'modified' => '2010-01-07 03:36:33', 68 | 'sort' => '0', 69 | 'active' => 1, 70 | ], 71 | ]; 72 | 73 | } 74 | -------------------------------------------------------------------------------- /tests/Fixture/SessionsFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'string', 'length' => 128], 20 | 'data' => ['type' => 'binary', 'length' => TableSchema::LENGTH_MEDIUM, 'null' => true], 21 | 'expires' => ['type' => 'integer', 'length' => 11, 'null' => true], 22 | '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], 23 | ]; 24 | 25 | /** 26 | * records property 27 | * 28 | * @var array 29 | */ 30 | public array $records = []; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /tests/Fixture/SluggedArticlesFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer'], 16 | 'title' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => ''], 17 | 'slug' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => ''], 18 | 'long_title' => ['type' => 'string', 'null' => false, 'default' => ''], 19 | 'long_slug' => ['type' => 'string', 'null' => false, 'default' => ''], 20 | 'section' => ['type' => 'integer', 'null' => true], 21 | '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], 22 | ]; 23 | 24 | /** 25 | * records property 26 | * 27 | * @var array 28 | */ 29 | public array $records = [ 30 | [ 31 | 'title' => 'Foo', 32 | 'slug' => 'foo', 33 | 'long_title' => 'Foo Bar', 34 | 'long_slug' => 'foo-bar', 35 | 'section' => null, 36 | ], 37 | ]; 38 | 39 | } 40 | -------------------------------------------------------------------------------- /tests/Fixture/StoriesFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer'], 19 | 'title' => ['type' => 'string', 'null' => false, 'length' => 64, 'comment' => ''], 20 | 'slug' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 20, 'comment' => ''], 21 | 'created' => ['type' => 'datetime', 'null' => true, 'default' => null, 'collate' => null, 'comment' => ''], 22 | 'modified' => ['type' => 'datetime', 'null' => true, 'default' => null, 'collate' => null, 'comment' => ''], 23 | 'sort' => ['type' => 'integer', 'null' => false, 'default' => '0', 'length' => 10, 'collate' => null, 'comment' => ''], 24 | 'active' => ['type' => 'boolean', 'null' => false, 'default' => false, 'collate' => null, 'comment' => ''], 25 | '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], 26 | ]; 27 | 28 | /** 29 | * Records 30 | * 31 | * @var array 32 | */ 33 | public array $records = [ 34 | [ 35 | 'id' => '1', 36 | 'title' => 'Second', 37 | 'slug' => 'second', 38 | 'created' => '2010-01-07 03:36:32', 39 | 'modified' => '2010-01-07 03:36:33', 40 | 'sort' => '2', 41 | 'active' => 1, 42 | ], 43 | [ 44 | 'id' => '2', 45 | 'title' => 'Third', 46 | 'slug' => 'third', 47 | 'created' => '2010-01-07 03:36:33', 48 | 'modified' => '2010-01-07 03:36:33', 49 | 'sort' => '3', 50 | 'active' => 1, 51 | ], 52 | [ 53 | 'id' => '3', 54 | 'title' => 'First', 55 | 'slug' => 'first', 56 | 'created' => '2010-01-07 03:36:31', 57 | 'modified' => '2010-01-07 03:36:33', 58 | 'sort' => '4', 59 | 'active' => 1, 60 | ], 61 | [ 62 | 'id' => '4', 63 | 'title' => 'Forth', 64 | 'slug' => 'forth', 65 | 'created' => '2010-01-07 03:36:34', 66 | 'modified' => '2010-01-07 03:36:33', 67 | 'sort' => '1', 68 | 'active' => 1, 69 | ], 70 | ]; 71 | 72 | } 73 | -------------------------------------------------------------------------------- /tests/Fixture/StringCommentsFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer'], 14 | 'comment' => ['type' => 'string', 'length' => 255, 'null' => false], 15 | 'url' => ['type' => 'string', 'length' => 255, 'null' => false], 16 | 'title' => ['type' => 'string', 'length' => 255, 'null' => false], 17 | '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], 18 | ]; 19 | 20 | /** 21 | * @var array 22 | */ 23 | public array $records = [ 24 | ]; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /tests/Fixture/ToggleAddressesFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer', 'length' => 10, 'unsigned' => true, 'null' => false, 'default' => null, 'autoIncrement' => true, 'precision' => null], 16 | 'category_id' => ['type' => 'integer', 'length' => 10, 'unsigned' => true, 'null' => true, 'default' => null, 'precision' => null, 'autoIncrement' => null], 17 | 'name' => ['type' => 'string', 'length' => 60, 'null' => false, 'default' => null, 'precision' => null, 'fixed' => null], 18 | 'primary' => ['type' => 'boolean', 'length' => null, 'null' => false, 'default' => '0', 'precision' => null], 19 | 'created' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'precision' => null], 20 | 'modified' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'precision' => null], 21 | '_constraints' => [ 22 | 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], 23 | ], 24 | ]; 25 | 26 | /** 27 | * Records 28 | * 29 | * @var array 30 | */ 31 | public array $records = [ 32 | [ 33 | 'category_id' => 1, 34 | 'name' => 'Foo', 35 | 'primary' => 1, 36 | 'created' => '2017-04-02 15:45:33', 37 | 'modified' => '2017-04-02 15:45:33', 38 | ], 39 | ]; 40 | 41 | } 42 | -------------------------------------------------------------------------------- /tests/Fixture/ToolsUsersFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer'], 19 | 'name' => ['type' => 'string', 'null' => true], 20 | 'password' => ['type' => 'string', 'null' => true], 21 | 'role_id' => ['type' => 'integer', 'null' => true], 22 | '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], 23 | ]; 24 | 25 | /** 26 | * Records property 27 | * 28 | * @var array 29 | */ 30 | public array $records = [ 31 | ['role_id' => 1, 'password' => '123456', 'name' => 'User 1'], 32 | ['role_id' => 2, 'password' => '123456', 'name' => 'User 2'], 33 | ['role_id' => 1, 'password' => '123456', 'name' => 'User 3'], 34 | ['role_id' => 3, 'password' => '123456', 'name' => 'User 4'], 35 | ]; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /tests/TestCase/Authenticator/LoginLinkAuthenticatorTest.php: -------------------------------------------------------------------------------- 1 | identifiers = new IdentifierCollection([ 43 | 'Tools.LoginLink' => [ 44 | 'resolver' => [ 45 | 'className' => 'Authentication.Orm', 46 | 'userModel' => 'ToolsUsers', 47 | ], 48 | ], 49 | ]); 50 | } 51 | 52 | /** 53 | * @return void 54 | */ 55 | public function testAuthenticate() { 56 | $user = $this->fetchTable('ToolsUsers')->find()->firstOrFail(); 57 | 58 | $tokensTable = $this->fetchTable('Tools.Tokens'); 59 | $tokensTable->newKey('login_link', '123', $user->id); 60 | 61 | $this->request = ServerRequestFactory::fromGlobals( 62 | ['REQUEST_URI' => '/'], 63 | ); 64 | $this->request = $this->request->withQueryParams([ 65 | 'token' => '123', 66 | ]); 67 | 68 | $authenticator = new LoginLinkAuthenticator($this->identifiers); 69 | 70 | $result = $authenticator->authenticate($this->request); 71 | $this->assertInstanceOf(Result::class, $result); 72 | $this->assertSame(Result::SUCCESS, $result->getStatus()); 73 | $this->assertInstanceOf(ArrayAccess::class, $result->getData()); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /tests/TestCase/Command/InflectCommandTest.php: -------------------------------------------------------------------------------- 1 | loadPlugins(['Tools']); 26 | //$this->useCommandRunner(); 27 | } 28 | 29 | /** 30 | * Test execute method 31 | * 32 | * @return void 33 | */ 34 | public function testExecute(): void { 35 | $this->exec('inflect FooBar all'); 36 | 37 | $output = $this->_out->messages(); 38 | $expected = [ 39 | 0 => 'FooBar', 40 | 1 => 'Pluralized form : FooBars', 41 | 2 => 'Singular form : FooBar', 42 | 3 => 'CamelCase form : FooBar', 43 | 4 => 'under_scored_form : foo_bar', 44 | 5 => 'Human Readable Group form : FooBar', 45 | 6 => 'table_names form : foo_bars', 46 | 7 => 'Cake Model Class form : FooBar', 47 | 8 => 'variableForm : fooBar', 48 | 9 => 'Dasherized-form : foo-bar', 49 | ]; 50 | $this->assertSame($expected, $output); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /tests/TestCase/Controller/Admin/HelperControllerTest.php: -------------------------------------------------------------------------------- 1 | disableErrorHandlerMiddleware(); 19 | 20 | $this->get(['prefix' => 'Admin', 'plugin' => 'Tools', 'controller' => 'Helper', 'action' => 'chars']); 21 | 22 | $this->assertResponseCode(200); 23 | $this->assertNoRedirect(); 24 | } 25 | 26 | /** 27 | * @return void 28 | */ 29 | public function testCharsPost() { 30 | $this->disableErrorHandlerMiddleware(); 31 | 32 | $data = [ 33 | 'string' => 'Some string', 34 | ]; 35 | $this->post(['prefix' => 'Admin', 'plugin' => 'Tools', 'controller' => 'Helper', 'action' => 'chars'], $data); 36 | 37 | $this->assertResponseCode(200); 38 | $this->assertNoRedirect(); 39 | } 40 | 41 | /** 42 | * @return void 43 | */ 44 | public function testBitmasks() { 45 | $this->disableErrorHandlerMiddleware(); 46 | 47 | $this->get(['prefix' => 'Admin', 'plugin' => 'Tools', 'controller' => 'Helper', 'action' => 'bitmasks']); 48 | 49 | $this->assertResponseCode(200); 50 | $this->assertNoRedirect(); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /tests/TestCase/Controller/Admin/PagesControllerTest.php: -------------------------------------------------------------------------------- 1 | disableErrorHandlerMiddleware(); 19 | 20 | $this->get(['prefix' => 'Admin', 'plugin' => 'Tools', 'controller' => 'Pages', 'action' => 'index']); 21 | 22 | $this->assertResponseCode(200); 23 | $this->assertNoRedirect(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /tests/TestCase/Controller/Admin/ToolsControllerTest.php: -------------------------------------------------------------------------------- 1 | disableErrorHandlerMiddleware(); 19 | 20 | $this->get(['prefix' => 'Admin', 'plugin' => 'Tools', 'controller' => 'Tools', 'action' => 'index']); 21 | 22 | $this->assertResponseCode(200); 23 | $this->assertNoRedirect(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /tests/TestCase/Controller/Component/RefererRedirectComponentTest.php: -------------------------------------------------------------------------------- 1 | withQueryParams(['ref' => '/somewhere-else']); 34 | 35 | $this->event = new Event('Controller.beforeFilter'); 36 | $this->Controller = new RefererRedirectComponentTestController($serverRequest); 37 | 38 | Configure::write('App.fullBaseUrl', 'http://localhost'); 39 | } 40 | 41 | /** 42 | * @return void 43 | */ 44 | public function tearDown(): void { 45 | parent::tearDown(); 46 | 47 | unset($this->Controller); 48 | } 49 | 50 | /** 51 | * @return void 52 | */ 53 | public function testBeforeRedirect() { 54 | $response = new Response(); 55 | 56 | $componentRegistry = new ComponentRegistry($this->Controller); 57 | $refererRedirectComponent = new RefererRedirectComponent($componentRegistry); 58 | 59 | $refererRedirectComponent->beforeRedirect($this->event, ['action' => 'foo'], $response); 60 | /** @var \Cake\Http\Response $modifiedResponse */ 61 | $modifiedResponse = $this->event->getResult(); 62 | 63 | $this->assertSame(['/somewhere-else'], $modifiedResponse->getHeader('Location')); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /tests/TestCase/Controller/ControllerTest.php: -------------------------------------------------------------------------------- 1 | Controller = new Controller(new ServerRequest()); 31 | $this->Controller->startupProcess(); 32 | } 33 | 34 | /** 35 | * @return void 36 | */ 37 | public function tearDown(): void { 38 | parent::tearDown(); 39 | 40 | unset($this->Controller); 41 | } 42 | 43 | /** 44 | * @return void 45 | */ 46 | public function testPaginate() { 47 | Configure::write('Paginator.limit', 2); 48 | 49 | $ToolsUser = $this->getTableLocator()->get('ToolsUsers'); 50 | 51 | $count = $ToolsUser->find()->count(); 52 | $this->assertTrue($count > 3); 53 | 54 | $toolsUsers = $this->Controller->fetchTable('ToolsUsers'); 55 | $result = $this->Controller->paginate($toolsUsers); 56 | $this->assertSame(2, count($result->toArray())); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /tests/TestCase/Controller/ShuntRequestControllerTest.php: -------------------------------------------------------------------------------- 1 | loadPlugins(['Tools']); 30 | Configure::write('Config.allowedLanguages', []); 31 | Configure::write('Config.defaultLanguage', null); 32 | } 33 | 34 | /** 35 | * @return void 36 | */ 37 | public function testLanguage() { 38 | $this->disableErrorHandlerMiddleware(); 39 | 40 | Configure::write('Config.defaultLanguage', 'de'); 41 | Configure::write('Config.allowedLanguages', [ 42 | 'de' => [ 43 | 'locale' => 'de_DE', 44 | 'name' => 'Deutsch', 45 | ], 46 | ]); 47 | 48 | $this->post(['plugin' => 'Tools', 'controller' => 'ShuntRequest', 'action' => 'language']); 49 | 50 | $this->assertRedirect(); 51 | } 52 | 53 | /** 54 | * @return void 55 | */ 56 | public function testLanguageError() { 57 | $this->disableErrorHandlerMiddleware(); 58 | 59 | $this->expectException(RuntimeException::class); 60 | 61 | $this->post(['plugin' => 'Tools', 'controller' => 'ShuntRequest', 'action' => 'language']); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /tests/TestCase/ErrorHandler/ErrorLoggerTest.php: -------------------------------------------------------------------------------- 1 | errorLogger = new ErrorLogger(); 27 | } 28 | 29 | /** 30 | * @return void 31 | */ 32 | public function testIs404(): void { 33 | $exception = new NotFoundException(); 34 | $result = $this->invokeMethod($this->errorLogger, 'is404', [$exception]); 35 | 36 | $this->assertTrue($result); 37 | 38 | $exception = new InternalErrorException(); 39 | $result = $this->invokeMethod($this->errorLogger, 'is404', [$exception]); 40 | $this->assertFalse($result); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /tests/TestCase/ErrorHandler/ExceptionTrapTest.php: -------------------------------------------------------------------------------- 1 | exceptionTrap = new ExceptionTrap(); 23 | } 24 | 25 | /** 26 | * @return void 27 | */ 28 | public function testLogger(): void { 29 | $result = $this->exceptionTrap->getConfig('logger'); 30 | $this->assertSame(ErrorLogger::class, $result); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /tests/TestCase/Form/ContactFormTest.php: -------------------------------------------------------------------------------- 1 | Form = new ContactForm(); 34 | } 35 | 36 | /** 37 | * Test testValidate 38 | * 39 | * @return void 40 | */ 41 | public function testValidate() { 42 | $requestData = [ 43 | 'name' => 'Foo', 44 | 'email' => 'foo', 45 | 'subject' => '', 46 | 'body' => 'Some message', 47 | ]; 48 | $result = $this->Form->validate($requestData); 49 | $this->assertFalse($result); 50 | 51 | $errors = $this->Form->getErrors(); 52 | $this->assertSame(['email', 'subject'], array_keys($errors)); 53 | 54 | $requestData = [ 55 | 'name' => 'Foo', 56 | 'email' => 'foo@example.org', 57 | 'subject' => 'Yeah', 58 | 'body' => 'Some message', 59 | ]; 60 | $result = $this->Form->validate($requestData); 61 | $this->assertTrue($result); 62 | } 63 | 64 | /** 65 | * Test testExecute 66 | * 67 | * @return void 68 | */ 69 | public function testExecute() { 70 | $requestData = [ 71 | 'name' => 'Foo', 72 | 'email' => 'foo@example.org', 73 | 'subject' => 'Yeah', 74 | 'body' => 'Some message', 75 | ]; 76 | $result = $this->Form->execute($requestData); 77 | $this->assertTrue($result); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /tests/TestCase/HtmlDom/HtmlDomTest.php: -------------------------------------------------------------------------------- 1 | skipIf(true, 'Broken'); 22 | } 23 | 24 | /** 25 | * HtmlDom test 26 | * 27 | * @return void 28 | */ 29 | public function testBasics() { 30 | $this->HtmlDom = new HtmlDom('
Hello
World
'); 31 | $result = $this->HtmlDom->find('div', 1)->innertext; 32 | $this->assertSame('World', $result); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /tests/TestCase/I18n/DateTest.php: -------------------------------------------------------------------------------- 1 | Time = new Date(); 20 | 21 | parent::setUp(); 22 | } 23 | 24 | /** 25 | * TimeTest::testObject() 26 | * 27 | * @return void 28 | */ 29 | public function testObject() { 30 | $this->assertTrue(is_object($this->Time)); 31 | $this->assertInstanceOf(Date::class, $this->Time); 32 | } 33 | 34 | /** 35 | * @return void 36 | */ 37 | public function testDate() { 38 | $from = '2012-12-31'; 39 | $Date = new Date($from); 40 | $this->assertSame($from, $Date->format(FORMAT_DB_DATE)); 41 | 42 | $from = ['year' => 2012, 'month' => 12, 'day' => 31]; 43 | $this->assertSame('2012-12-31', $Date->format(FORMAT_DB_DATE)); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /tests/TestCase/I18n/DateTimeHelperTest.php: -------------------------------------------------------------------------------- 1 | 2009, 16 | 'month' => 12, 17 | 'day' => 1, 18 | 'hour' => 0, 19 | 'minute' => 0, 20 | 'second' => 0, 21 | ]; 22 | $result = DateTimeHelper::constructDatetime($value); 23 | $this->assertSame('2009-12-01 00:00:00', $result); 24 | 25 | $value = [ 26 | 'year' => 2009, 27 | ]; 28 | $result = DateTimeHelper::constructDatetime($value); 29 | $this->assertSame('', $result); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /tests/TestCase/Identifier/LoginLinkIdentifierTest.php: -------------------------------------------------------------------------------- 1 | fetchTable('ToolsUsers')->find()->firstOrFail(); 24 | 25 | $identifier = new LoginLinkIdentifier([ 26 | 'resolver' => [ 27 | 'className' => 'Authentication.Orm', 28 | 'userModel' => 'ToolsUsers', 29 | ], 30 | ]); 31 | $result = $identifier->identify(['id' => $user->id]); 32 | $this->assertInstanceOf(Entity::class, $result); 33 | } 34 | 35 | /** 36 | * @return void 37 | */ 38 | public function testIdentifyPreCallback() { 39 | $user = $this->fetchTable('ToolsUsers')->find()->firstOrFail(); 40 | $this->assertNotEmpty($user->password); 41 | 42 | $identifier = new LoginLinkIdentifier([ 43 | 'resolver' => [ 44 | 'className' => 'Authentication.Orm', 45 | 'userModel' => 'ToolsUsers', 46 | ], 47 | 'preCallback' => function (int $id) { 48 | $fields = ['password' => null]; 49 | $conditions = compact('id'); 50 | TableRegistry::getTableLocator()->get('ToolsUsers')->updateAll($fields, $conditions); 51 | }, 52 | ]); 53 | $result = $identifier->identify(['id' => $user->id]); 54 | $this->assertInstanceOf(Entity::class, $result); 55 | 56 | $this->assertEmpty($result->password); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /tests/TestCase/Mailer/EmailTest.php: -------------------------------------------------------------------------------- 1 | skipIf(true, 'Will be removed'); 29 | 30 | TransportFactory::setConfig('debug', [ 31 | 'className' => 'Debug', 32 | ]); 33 | 34 | Configure::delete('Config.xMailer'); 35 | } 36 | 37 | /** 38 | * tearDown method 39 | * 40 | * @return void 41 | */ 42 | public function tearDown(): void { 43 | parent::tearDown(); 44 | Log::drop('email'); 45 | //Email::drop('test'); 46 | TransportFactory::drop('debug'); 47 | TransportFactory::drop('test_smtp'); 48 | 49 | Configure::delete('Config.xMailer'); 50 | } 51 | 52 | /** 53 | * @return void 54 | */ 55 | public function testSetProfile() { 56 | Configure::write('Config.xMailer', 'foobar'); 57 | 58 | $this->Email->setProfile('default'); 59 | 60 | $result = $this->Email->getMessage()->getHeaders(); 61 | $this->assertArrayHasKey('X-Mailer', $result); 62 | $this->assertSame('foobar', $result['X-Mailer']); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Behavior/EncryptionBehaviorTest.php: -------------------------------------------------------------------------------- 1 | table = $this->getTableLocator()->get('Sessions'); 29 | $this->table->addBehavior('Tools.Encryption', [ 30 | 'fields' => ['data'], 31 | 'key' => 'some-very-long-key-which-needs-to-be-at-least-32-chars-long', 32 | ]); 33 | } 34 | 35 | /** 36 | * @return void 37 | */ 38 | public function testSaveBasic() { 39 | $data = [ 40 | 'id' => 10, 41 | 'data' => 'test save', 42 | ]; 43 | 44 | $entity = $this->table->newEntity($data); 45 | $entityAfter = $this->table->save($entity); 46 | $this->assertTrue((bool)$entityAfter); 47 | 48 | $connection = ConnectionManager::get('default'); 49 | $result = $connection->getDriver()->execute('SELECT data FROM sessions WHERE id = :id', ['id' => $entityAfter->id])->fetchAll(); 50 | $this->assertNotEquals($data['data'], $result[0][0]); 51 | } 52 | 53 | /** 54 | * @return void 55 | */ 56 | public function testFindBasic() { 57 | $data = [ 58 | 'id' => 10, 59 | 'data' => 'test save', 60 | ]; 61 | $entity = $this->table->newEntity($data); 62 | $this->table->save($entity); 63 | 64 | $entity = $this->table->get(10); 65 | $this->assertEquals('test save', $entity->data); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Behavior/NeighborBehaviorTest.php: -------------------------------------------------------------------------------- 1 | Table = $this->getTableLocator()->get('Stories'); 28 | $this->Table->addBehavior('Tools.Neighbor'); 29 | } 30 | 31 | /** 32 | * @return void 33 | */ 34 | public function tearDown(): void { 35 | $this->getTableLocator()->clear(); 36 | 37 | parent::tearDown(); 38 | } 39 | 40 | /** 41 | * @return void 42 | */ 43 | public function testNeighbors() { 44 | $id = 2; 45 | 46 | $result = $this->Table->neighbors($id); 47 | 48 | $this->assertEquals('Second', $result['prev']['title']); 49 | $this->assertEquals('Forth', $result['next']['title']); 50 | } 51 | 52 | /** 53 | * @return void 54 | */ 55 | public function testNeighborsReverse() { 56 | $id = 2; 57 | 58 | $result = $this->Table->neighbors($id, ['reverse' => true]); 59 | 60 | $this->assertEquals('Forth', $result['prev']['title']); 61 | $this->assertEquals('Second', $result['next']['title']); 62 | } 63 | 64 | /** 65 | * @return void 66 | */ 67 | public function testNeighborsCustomSortField() { 68 | $id = 2; 69 | 70 | $result = $this->Table->neighbors($id, ['sortField' => 'sort']); 71 | 72 | $this->assertEquals('Second', $result['prev']['title']); 73 | $this->assertEquals('First', $result['next']['title']); 74 | } 75 | 76 | /** 77 | * @return void 78 | */ 79 | public function testNeighborsCustomFields() { 80 | $id = 2; 81 | 82 | $result = $this->Table->neighbors($id, ['sortField' => 'sort', 'fields' => ['title']]); 83 | $this->assertEquals(['title' => 'Second'], $result['prev']->toArray()); 84 | $this->assertEquals(['title' => 'First'], $result['next']->toArray()); 85 | } 86 | 87 | /** 88 | * @return void 89 | */ 90 | public function testNeighborsStart() { 91 | $id = 1; 92 | 93 | $result = $this->Table->neighbors($id, ['sortField' => 'id']); 94 | 95 | $this->assertNull($result['prev']); 96 | $this->assertEquals('Third', $result['next']['title']); 97 | } 98 | 99 | /** 100 | * @return void 101 | */ 102 | public function testNeighborsEnd() { 103 | $id = 4; 104 | 105 | $result = $this->Table->neighbors($id); 106 | $this->assertEquals('Third', $result['prev']['title']); 107 | $this->assertNull($result['next']); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Behavior/TypeMapBehaviorTest.php: -------------------------------------------------------------------------------- 1 | Table = $this->getTableLocator()->get('Data'); 35 | $this->Table->addBehavior('Tools.Jsonable', ['fields' => ['data_array']]); 36 | 37 | $entity = $this->Table->newEmptyEntity(); 38 | 39 | $data = [ 40 | 'name' => 'FooBar', 41 | 'data_json' => ['x' => 'y'], 42 | 'data_array' => ['x' => 'y'], 43 | ]; 44 | $entity = $this->Table->patchEntity($entity, $data); 45 | $this->assertEmpty($entity->getErrors()); 46 | 47 | $this->Table->saveOrFail($entity); 48 | $this->assertSame($data['data_json'], $entity->data_json); 49 | $this->assertSame('{"x":"y"}', $entity->data_array); 50 | 51 | $savedEntity = $this->Table->get($entity->id); 52 | 53 | $this->assertSame($data['data_json'], $savedEntity->data_json); 54 | $this->assertSame($data['data_array'], $savedEntity->data_array); 55 | 56 | // Now let's disable the array conversion per type 57 | $this->Table->removeBehavior('Jsonable'); 58 | $this->Table->addBehavior('Tools.TypeMap', ['fields' => ['data_json' => 'text']]); 59 | $entity = $this->Table->get($entity->id); 60 | 61 | $this->assertSame('{"x":"y"}', $entity->data_json); 62 | $this->assertSame('{"x":"y"}', $entity->data_array); 63 | 64 | $data = [ 65 | 'data_json' => '{"x":"z"}', 66 | 'data_array' => '{"x":"z"}', 67 | ]; 68 | $entity = $this->Table->patchEntity($entity, $data); 69 | $this->Table->saveOrFail($entity); 70 | 71 | $savedEntity = $this->Table->get($entity->id); 72 | $this->assertSame($data['data_json'], $savedEntity->data_json); 73 | $this->assertSame($data['data_array'], $savedEntity->data_array); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Entity/EntityTest.php: -------------------------------------------------------------------------------- 1 | Users = $this->getTableLocator()->get('ToolsUsers'); 30 | } 31 | 32 | /** 33 | * @return void 34 | */ 35 | public function tearDown(): void { 36 | $this->getTableLocator()->clear(); 37 | 38 | parent::tearDown(); 39 | } 40 | 41 | /** 42 | * @return void 43 | */ 44 | public function testEnum() { 45 | $array = [ 46 | 1 => 'foo', 47 | 2 => 'bar', 48 | ]; 49 | 50 | $res = Entity::enum(null, $array); 51 | $this->assertSame($array, $res); 52 | 53 | $res = Entity::enum(2, $array); 54 | $this->assertSame('bar', $res); 55 | 56 | $res = Entity::enum('2', $array); 57 | $this->assertSame('bar', $res); 58 | 59 | $res = Entity::enum(3, $array); 60 | $this->assertNull($res); 61 | } 62 | 63 | /** 64 | * @return void 65 | */ 66 | public function testEnumPartialOptions() { 67 | $array = [ 68 | 1 => 'foo', 69 | 2 => 'bar', 70 | 3 => 'yeah', 71 | ]; 72 | 73 | $res = Entity::enum([2, 3], $array); 74 | $expected = $array; 75 | unset($expected[1]); 76 | $this->assertSame($expected, $res); 77 | } 78 | 79 | /** 80 | * @return void 81 | */ 82 | public function testEnumDefaultValue() { 83 | $array = [ 84 | 1 => 'foo', 85 | 2 => 'bar', 86 | ]; 87 | 88 | $res = Entity::enum(null, $array, false); 89 | $this->assertSame($array, $res); 90 | 91 | $res = Entity::enum(2, $array, false); 92 | $this->assertSame('bar', $res); 93 | 94 | $res = Entity::enum('2', $array, false); 95 | $this->assertSame('bar', $res); 96 | 97 | $res = Entity::enum(3, $array, false); 98 | $this->assertFalse($res); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Enum/EnumOptionsTraitTest.php: -------------------------------------------------------------------------------- 1 | 'One', 18 | 2 => 'Two', 19 | ]; 20 | 21 | $res = FooBar::options([1, 2]); 22 | $expected = $array; 23 | $this->assertSame($expected, $res); 24 | } 25 | 26 | /** 27 | * @return void 28 | */ 29 | public function testEnumResorting() { 30 | $array = [ 31 | 2 => 'Two', 32 | 1 => 'One', 33 | ]; 34 | 35 | $res = FooBar::options([FooBar::Two, FooBar::One], $array); 36 | $expected = $array; 37 | $this->assertSame($expected, $res); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Table/TokensTableTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | protected array $fixtures = [ 14 | 'plugin.Tools.Tokens', 15 | ]; 16 | 17 | /** 18 | * @var \Tools\Model\Table\TokensTable 19 | */ 20 | protected $Tokens; 21 | 22 | /** 23 | * @return void 24 | */ 25 | public function setUp(): void { 26 | parent::setUp(); 27 | 28 | $this->Tokens = $this->getTableLocator()->get('Tools.Tokens'); 29 | } 30 | 31 | /** 32 | * @return void 33 | */ 34 | public function tearDown(): void { 35 | $this->getTableLocator()->clear(); 36 | 37 | parent::tearDown(); 38 | } 39 | 40 | /** 41 | * @return void 42 | */ 43 | public function testTokenInstance() { 44 | $this->assertInstanceOf(TokensTable::class, $this->Tokens); 45 | } 46 | 47 | /** 48 | * @return void 49 | */ 50 | public function testGenerateKey() { 51 | $key = $this->Tokens->generateKey(4); 52 | $this->assertTrue(!empty($key) && strlen($key) === 4); 53 | } 54 | 55 | /** 56 | * @return void 57 | */ 58 | public function testNewKeySpendKey() { 59 | $key = $this->Tokens->newKey('test', null, null, 'xyz'); 60 | $this->assertTrue(!empty($key)); 61 | 62 | $res = $this->Tokens->useKey('test', $key); 63 | $this->assertTrue(!empty($res)); 64 | 65 | $res = $this->Tokens->useKey('test', $key); 66 | $this->assertTrue(!empty($res) && !empty($res->used)); 67 | 68 | $res = $this->Tokens->useKey('test', $key . 'x'); 69 | $this->assertNull($res); 70 | 71 | $res = $this->Tokens->useKey('testx', $key); 72 | $this->assertNull($res); 73 | } 74 | 75 | /** 76 | * @return void 77 | */ 78 | public function testGarbageCollector() { 79 | $data = [ 80 | 'created' => date(FORMAT_DB_DATETIME, time() - 3 * MONTH), 81 | 'type' => 'y', 82 | 'token' => 'x', 83 | ]; 84 | $entity = $this->Tokens->newEntity($data, ['validate' => false]); 85 | $this->Tokens->save($entity); 86 | $count = $this->Tokens->find()->count(); 87 | $this->Tokens->garbageCollector(); 88 | $count2 = $this->Tokens->find()->count(); 89 | $this->assertTrue($count > $count2); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /tests/TestCase/Utility/ClockTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(round($res, 1) === 0.2); 19 | 20 | time_nanosleep(0, 100000000); 21 | $res = Clock::returnElapsedTime(8, true); 22 | $this->assertTrue(round($res, 1) === 0.3); 23 | 24 | time_nanosleep(0, 100000000); 25 | $res = Clock::returnElapsedTime(); 26 | $this->assertTrue(round($res, 1) === 0.1); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /tests/TestCase/Utility/MimeTypesTest.php: -------------------------------------------------------------------------------- 1 | MimeTypes = new MimeTypes(); 22 | } 23 | 24 | /** 25 | * @return void 26 | */ 27 | public function testAll() { 28 | $res = $this->MimeTypes->all(); 29 | $this->assertTrue(is_array($res) && count($res) > 100); 30 | } 31 | 32 | /** 33 | * @return void 34 | */ 35 | public function testSingle() { 36 | $res = $this->MimeTypes->getMimeType('odxs'); 37 | $this->assertNull($res); 38 | 39 | $res = $this->MimeTypes->getMimeType('ods'); 40 | $this->assertEquals('application/vnd.oasis.opendocument.spreadsheet', $res); 41 | } 42 | 43 | /** 44 | * @return void 45 | */ 46 | public function testOverwrite() { 47 | $res = $this->MimeTypes->getMimeType('ics'); 48 | $this->assertEquals('application/ics', $res); 49 | } 50 | 51 | /** 52 | * @return void 53 | */ 54 | public function testReverseToSingle() { 55 | $res = $this->MimeTypes->getMimeType('html'); 56 | $this->assertEquals('text/html', $res); 57 | 58 | $res = $this->MimeTypes->getMimeType('csv'); 59 | $this->assertEquals('text/csv', $res); 60 | } 61 | 62 | /** 63 | * @return void 64 | */ 65 | public function testReverseToMultiple() { 66 | $res = $this->MimeTypes->getMimeType('html', false); 67 | $this->assertTrue(is_array($res)); 68 | $this->assertSame(2, count($res)); 69 | 70 | $res = $this->MimeTypes->getMimeType('csv', false); 71 | $this->assertTrue(is_array($res)); // && count($res) > 2 72 | $this->assertSame(2, count($res)); 73 | } 74 | 75 | /** 76 | * @return void 77 | */ 78 | public function test(): void { 79 | $result = $this->MimeTypes->mapType('application/pdf'); 80 | $this->assertSame('pdf', $result); 81 | 82 | $result = $this->MimeTypes->mapType('application/pdf123'); 83 | $this->assertNull($result); 84 | 85 | $result = $this->MimeTypes->mapType('image/x-tiff'); 86 | $this->assertSame('tif', $result); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /tests/TestCase/Utility/RandomTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($is >= 2 && $is <= 200); 17 | } 18 | 19 | /** 20 | * @return void 21 | */ 22 | public function testArrayValue() { 23 | $array = [ 24 | 'x', 25 | 'y', 26 | 'z', 27 | ]; 28 | $is = Random::arrayValue($array, null, null, true); 29 | $this->assertTrue(in_array($is, $array)); 30 | 31 | // non-numerical indexes 32 | $array = [ 33 | 'e' => 'x', 34 | 'f' => 'y', 35 | 'g' => 'z', 36 | ]; 37 | $is = Random::arrayValue($array); 38 | $this->assertTrue(in_array($is, $array)); 39 | } 40 | 41 | /** 42 | * @return void 43 | */ 44 | public function testPwd() { 45 | $result = Random::pwd(10); 46 | $this->assertTrue(mb_strlen($result) === 10); 47 | } 48 | 49 | /** 50 | * @return void 51 | */ 52 | public function testPronounceablePwd() { 53 | $is = Random::pronounceablePwd(6); 54 | //pr($is); 55 | $this->assertTrue(strlen($is) === 6); 56 | 57 | $is = Random::pronounceablePwd(11); 58 | //pr($is); 59 | $this->assertTrue(strlen($is) === 11); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /tests/TestCase/Utility/TextTest.php: -------------------------------------------------------------------------------- 1 | Text = new Text(); 22 | } 23 | 24 | /** 25 | * @return void 26 | */ 27 | public function testReadTab() { 28 | $data = <<Text->readTab($data); 33 | 34 | $this->assertSame(2, count($result)); 35 | $this->assertSame(['and', 'another', 'line'], $result[1]); 36 | } 37 | 38 | /** 39 | * @return void 40 | */ 41 | public function testReadWithPattern() { 42 | $data = <<Text->readWithPattern($data, '%s %s %s'); 48 | 49 | $this->assertSame(3, count($result)); 50 | $this->assertSame(['and', 'a', 'third'], $result[2]); 51 | } 52 | 53 | /** 54 | * @return void 55 | */ 56 | public function testConvertToOrd() { 57 | $is = $this->Text->convertToOrd('h H'); 58 | $this->assertSame($is, '0-104-32-72-0'); 59 | 60 | $is = $this->Text->convertToOrd('x' . PHP_EOL . 'x' . PHP_EOL . 'x' . PHP_EOL . 'x' . PHP_EOL . 'x' . "\t" . 'x'); 61 | $this->assertSame('0-120-10-120-10-120-10-120-10-120-9-120-0', $is); 62 | } 63 | 64 | /** 65 | * @return void 66 | */ 67 | public function testMaxWords() { 68 | $this->assertEquals('Taylor...', Text::maxWords('Taylor Otwell', 1)); 69 | $this->assertEquals('Taylor___', Text::maxWords('Taylor Otwell', 1, ['ellipsis' => '___'])); 70 | $this->assertEquals('Taylor Otwell', Text::maxWords('Taylor Otwell', 3)); 71 | } 72 | 73 | /** 74 | * @return void 75 | */ 76 | public function testWords() { 77 | $is = $this->Text->words('Hochhaus, Unter dem Bau von ae Äußeren Einflüssen - und von Autos.', ['min_char' => 3]); 78 | $this->assertTrue(!empty($is) && is_array($is) && count($is) === 9); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /tests/TestCase/View/Helper/FormHelperTest.php: -------------------------------------------------------------------------------- 1 | View = new View(null); 36 | $this->Form = new FormHelper($this->View); 37 | } 38 | 39 | /** 40 | * test novalidate for create 41 | * 42 | * @return void 43 | */ 44 | public function testCreate() { 45 | $expected = 'novalidate="novalidate"'; 46 | 47 | $result = $this->Form->create(); 48 | $this->assertStringNotContainsString($expected, $result); 49 | 50 | Configure::write('FormConfig.novalidate', true); 51 | $this->Form = new FormHelper($this->View); 52 | 53 | $result = $this->Form->create(); 54 | $this->assertStringContainsString($expected, $result); 55 | 56 | $result = $this->Form->create(null, ['novalidate' => false]); 57 | $this->assertStringNotContainsString($expected, $result); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /tests/TestCase/View/Helper/TimelineHelperTest.php: -------------------------------------------------------------------------------- 1 | Timeline = new TimelineHelper(new View(null)); 27 | } 28 | 29 | /** 30 | * @return void 31 | */ 32 | public function testAddItem() { 33 | $data = [ 34 | 'start' => null, 35 | 'content' => '', 36 | ]; 37 | $this->Timeline->addItem($data); 38 | $items = $this->Timeline->items(); 39 | $this->assertSame(1, count($items)); 40 | 41 | $data = [ 42 | [ 43 | 'start' => null, 44 | 'content' => '', 45 | ], 46 | [ 47 | 'start' => null, 48 | 'content' => '', 49 | ], 50 | ]; 51 | $this->Timeline->addItems($data); 52 | $items = $this->Timeline->items(); 53 | $this->assertSame(3, count($items)); 54 | } 55 | 56 | /** 57 | * @return void 58 | */ 59 | public function testFinalize() { 60 | $this->testAddItem(); 61 | $data = [ 62 | 'start' => new DateTime(), 63 | 'content' => '', 64 | ]; 65 | $this->Timeline->addItem($data); 66 | $data = [ 67 | 'start' => new DateTime(date(FORMAT_DB_DATE)), 68 | 'content' => '', 69 | ]; 70 | $this->Timeline->addItem($data); 71 | 72 | $this->Timeline->finalize(); 73 | $result = $this->Timeline->getView()->fetch('script'); 74 | $this->assertStringContainsString('\'start\': new Date(', $result); 75 | } 76 | 77 | /** 78 | * @return void 79 | */ 80 | public function testFinalizeReturnScript() { 81 | $this->testAddItem(); 82 | $data = [ 83 | 'start' => new DateTime(), 84 | 'content' => '', 85 | ]; 86 | $this->Timeline->addItem($data); 87 | $data = [ 88 | 'start' => new DateTime(date(FORMAT_DB_DATE)), 89 | 'content' => '', 90 | ]; 91 | $this->Timeline->addItem($data); 92 | 93 | $result = $this->Timeline->finalize(true); 94 | $this->assertStringContainsString('\'start\': new Date(', $result); 95 | } 96 | 97 | /** 98 | * @return void 99 | */ 100 | public function tearDown(): void { 101 | parent::tearDown(); 102 | 103 | unset($this->Timeline); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /tests/TestCase/View/Icon/BootstrapIconTest.php: -------------------------------------------------------------------------------- 1 | icon = new BootstrapIcon(); 22 | } 23 | 24 | /** 25 | * @return void 26 | */ 27 | public function testRender(): void { 28 | $result = $this->icon->render('info-circle-fill'); 29 | $this->assertSame('', $result); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /tests/TestCase/View/Icon/Collector/BootstrapIconCollectorTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(count($result) > 1360, 'count of ' . count($result)); 21 | $this->assertTrue(in_array('info-circle-fill', $result, true)); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /tests/TestCase/View/Icon/Collector/FeatherIconCollectorTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(count($result) > 280, 'count of ' . count($result)); 21 | $this->assertTrue(in_array('zoom-in', $result, true)); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /tests/TestCase/View/Icon/Collector/FontAwesome4CollectorTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(count($result) > 780, 'count of ' . count($result)); 25 | $this->assertTrue(in_array('thumbs-up', $result, true)); 26 | } 27 | 28 | /** 29 | * @return array 30 | */ 31 | public static function extensions(): array { 32 | return [ 33 | 'scss' => ['scss'], 34 | 'less' => ['less'], 35 | ]; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /tests/TestCase/View/Icon/Collector/FontAwesome5CollectorTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(count($result) > 1456, 'count of ' . count($result)); 21 | $this->assertTrue(in_array('thumbs-up', $result, true)); 22 | } 23 | 24 | /** 25 | * Show that we are still API compatible/valid. 26 | * 27 | * @return void 28 | */ 29 | public function testCollectSvg(): void { 30 | $path = TEST_FILES . 'font_icon' . DS . 'fa5' . DS . 'solid.svg'; 31 | 32 | $result = FontAwesome5IconCollector::collect($path); 33 | 34 | $this->assertTrue(count($result) > 1000, 'count of ' . count($result)); 35 | $this->assertTrue(in_array('thumbs-up', $result, true)); 36 | } 37 | 38 | /** 39 | * Show that we are still API compatible/valid. 40 | * 41 | * @return void 42 | */ 43 | public function testCollectYml(): void { 44 | $path = TEST_FILES . 'font_icon' . DS . 'fa5' . DS . 'icons.yml'; 45 | 46 | $result = FontAwesome5IconCollector::collect($path); 47 | 48 | $this->assertTrue(count($result) > 1400, 'count of ' . count($result)); 49 | $this->assertTrue(in_array('thumbs-up', $result, true)); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /tests/TestCase/View/Icon/Collector/FontAwesome6CollectorTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(count($result) > 1456, 'count of ' . count($result)); 21 | $this->assertTrue(in_array('thumbs-up', $result, true)); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /tests/TestCase/View/Icon/Collector/MaterialIconCollectorTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(count($result) > 2444, 'count of ' . count($result)); 21 | $this->assertTrue(in_array('zoom_in', $result, true)); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /tests/TestCase/View/Icon/FeatherIconTest.php: -------------------------------------------------------------------------------- 1 | icon = new FeatherIcon(); 22 | } 23 | 24 | /** 25 | * @return void 26 | */ 27 | public function testRender(): void { 28 | $result = $this->icon->render('view'); 29 | $this->assertSame('', $result); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /tests/TestCase/View/Icon/FontAwesome4IconTest.php: -------------------------------------------------------------------------------- 1 | icon = new FontAwesome4Icon(); 22 | } 23 | 24 | /** 25 | * @return void 26 | */ 27 | public function testRender(): void { 28 | $result = $this->icon->render('camera-retro'); 29 | $this->assertSame('', $result); 30 | } 31 | 32 | /** 33 | * @return void 34 | */ 35 | public function testRenderRotate(): void { 36 | $result = $this->icon->render('camera-retro', ['rotate' => 90]); 37 | $this->assertSame('', $result); 38 | } 39 | 40 | /** 41 | * @return void 42 | */ 43 | public function testRenderSpin(): void { 44 | $result = $this->icon->render('camera-retro', ['spin' => true]); 45 | $this->assertSame('', $result); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /tests/TestCase/View/Icon/FontAwesome5IconTest.php: -------------------------------------------------------------------------------- 1 | icon = new FontAwesome5Icon(); 22 | } 23 | 24 | /** 25 | * @return void 26 | */ 27 | public function testRender(): void { 28 | $result = $this->icon->render('camera-retro'); 29 | $this->assertSame('', $result); 30 | } 31 | 32 | /** 33 | * @return void 34 | */ 35 | public function testRenderLight(): void { 36 | $this->icon = new FontAwesome5Icon(['namespace' => 'fal']); 37 | 38 | $result = $this->icon->render('camera-retro'); 39 | $this->assertSame('', $result); 40 | } 41 | 42 | /** 43 | * @return void 44 | */ 45 | public function testRenderRotate(): void { 46 | $result = $this->icon->render('camera-retro', ['rotate' => 90]); 47 | $this->assertSame('', $result); 48 | } 49 | 50 | /** 51 | * @return void 52 | */ 53 | public function testRenderSpin(): void { 54 | $result = $this->icon->render('camera-retro', ['spin' => true]); 55 | $this->assertSame('', $result); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /tests/TestCase/View/Icon/FontAwesome6IconTest.php: -------------------------------------------------------------------------------- 1 | icon = new FontAwesome6Icon(); 22 | } 23 | 24 | /** 25 | * @return void 26 | */ 27 | public function testRender(): void { 28 | $result = $this->icon->render('camera-retro'); 29 | $this->assertSame('', $result); 30 | } 31 | 32 | /** 33 | * @return void 34 | */ 35 | public function testRenderLight(): void { 36 | $this->icon = new FontAwesome6Icon(['namespace' => 'light']); 37 | 38 | $result = $this->icon->render('camera-retro'); 39 | $this->assertSame('', $result); 40 | } 41 | 42 | /** 43 | * @return void 44 | */ 45 | public function testRenderRotate(): void { 46 | $result = $this->icon->render('camera-retro', ['rotate' => 90]); 47 | $this->assertSame('', $result); 48 | } 49 | 50 | /** 51 | * @return void 52 | */ 53 | public function testRenderSpin(): void { 54 | $result = $this->icon->render('camera-retro', ['spin' => true]); 55 | $this->assertSame('', $result); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /tests/TestCase/View/Icon/IconCollectionTest.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'feather' => [ 19 | 'class' => FeatherIcon::class, 20 | ], 21 | ], 22 | 'separator' => ':', 23 | ]; 24 | $result = (new IconCollection($config))->render('foo'); 25 | 26 | $this->assertSame('', $result); 27 | } 28 | 29 | /** 30 | * @return void 31 | */ 32 | public function testRenderNamespaced(): void { 33 | $config = [ 34 | 'sets' => [ 35 | 'feather' => [ 36 | 'class' => FeatherIcon::class, 37 | ], 38 | 'material' => [ 39 | 'class' => MaterialIcon::class, 40 | 'namespace' => 'material-symbols', 41 | 'attributes' => [ 42 | 'data-custom' => 'some-custom', 43 | ], 44 | ], 45 | ], 46 | 'separator' => ':', 47 | 'attributes' => [ 48 | 'data-default' => 'some-default', 49 | ], 50 | ]; 51 | $result = (new IconCollection($config))->render('material:foo'); 52 | 53 | $this->assertSame('foo', $result); 54 | } 55 | 56 | /** 57 | * @return void 58 | */ 59 | public function testNames(): void { 60 | $config = [ 61 | 'sets' => [ 62 | 'feather' => [ 63 | 'class' => FeatherIcon::class, 64 | 'path' => TEST_FILES . 'font_icon/feather/icons.json', 65 | ], 66 | 'material' => [ 67 | 'class' => MaterialIcon::class, 68 | 'path' => TEST_FILES . 'font_icon/material/index.d.ts', 69 | ], 70 | ], 71 | ]; 72 | $result = (new IconCollection($config))->names(); 73 | $this->assertTrue(count($result['material']) > 1740, 'count of ' . count($result['material'])); 74 | $this->assertTrue(in_array('zoom_out', $result['material'], true)); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /tests/TestCase/View/Icon/MaterialIconTest.php: -------------------------------------------------------------------------------- 1 | icon = new MaterialIcon(); 22 | } 23 | 24 | /** 25 | * @return void 26 | */ 27 | public function testRender(): void { 28 | $result = $this->icon->render('view'); 29 | $this->assertSame('view', $result); 30 | } 31 | 32 | /** 33 | * @return void 34 | */ 35 | public function testRenderNamespace(): void { 36 | $this->icon = new MaterialIcon(['namespace' => 'material-symbols-outlined']); 37 | 38 | $result = $this->icon->render('view'); 39 | $this->assertSame('view', $result); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /tests/config/bootstrap.php: -------------------------------------------------------------------------------- 1 | def(DashedRoute::class); 12 | 13 | $routes->scope('/', function(RouteBuilder $routes) { 14 | $routes->fallbacks(DashedRoute::class); 15 | }); 16 | 17 | $routes->prefix('Admin', function (RouteBuilder $routes) { 18 | $routes->plugin('Tools', function (RouteBuilder $routes) { 19 | $routes->connect('/', ['controller' => 'Tools', 'action' => 'index']); 20 | 21 | $routes->fallbacks(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/schema.php: -------------------------------------------------------------------------------- 1 | $iterator 9 | */ 10 | $iterator = new DirectoryIterator(__DIR__ . DS . 'Fixture'); 11 | foreach ($iterator as $file) { 12 | if (!preg_match('/(\w+)Fixture.php$/', (string)$file, $matches)) { 13 | continue; 14 | } 15 | 16 | $name = $matches[1]; 17 | $tableName = Inflector::underscore($name); 18 | $class = 'Tools\\Test\\Fixture\\' . $name . 'Fixture'; 19 | try { 20 | $object = (new ReflectionClass($class))->getProperty('fields'); 21 | } catch (ReflectionException $e) { 22 | continue; 23 | } 24 | 25 | $array = $object->getDefaultValue(); 26 | $constraints = $array['_constraints'] ?? []; 27 | $indexes = $array['_indexes'] ?? []; 28 | unset($array['_constraints'], $array['_indexes'], $array['_options']); 29 | $table = [ 30 | 'table' => $tableName, 31 | 'columns' => $array, 32 | 'constraints' => $constraints, 33 | 'indexes' => $indexes, 34 | ]; 35 | $tables[$tableName] = $table; 36 | } 37 | 38 | return $tables; 39 | -------------------------------------------------------------------------------- /tests/templates/email/html/welcome.php: -------------------------------------------------------------------------------- 1 | My price: ' . Number::format($value); 6 | -------------------------------------------------------------------------------- /tests/templates/email/text/welcome.php: -------------------------------------------------------------------------------- 1 | fetch('content'); 3 | -------------------------------------------------------------------------------- /tests/templates/layout/email/html/fancy.php: -------------------------------------------------------------------------------- 1 | fetch('content'); 4 | -------------------------------------------------------------------------------- /tests/templates/layout/email/text/fancy.php: -------------------------------------------------------------------------------- 1 | fetch('content'); 4 | --------------------------------------------------------------------------------