diff --git a/.github/workflows/code-quality-pr.yaml b/.github/workflows/code-quality-pr.yaml index 65a0f4646..af05451d1 100644 --- a/.github/workflows/code-quality-pr.yaml +++ b/.github/workflows/code-quality-pr.yaml @@ -6,25 +6,18 @@ on: jobs: codeQuality: runs-on: ubuntu-latest - name: PHP + name: PHPCS steps: - name: Checkout changes uses: actions/checkout@v1 - - name: Install PHP - uses: shivammathur/setup-php@master + - name: Install PHP and PHP Code Sniffer + uses: shivammathur/setup-php@v1 with: - php-version: 7.2 - - name: Install Composer dependencies - run: composer install --no-interaction --no-progress --no-suggest - - name: Reset October modules and library - run: | - git reset --hard HEAD - rm -rf ./vendor/october/rain - wget https://github.com/octobercms/library/archive/develop.zip -O ./vendor/october/develop.zip - unzip ./vendor/october/develop.zip -d ./vendor/october - mv ./vendor/october/library-develop ./vendor/october/rain - composer dump-autoload + php-version: '7.3' + tools: phpcs + - name: Setup problem matcher for PHPCS + run: echo "::add-matcher::${{ github.workspace }}/.github/workflows/matchers/phpcs-matcher.json" - name: Run code quality checks run: | git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" && git fetch - ./vendor/bin/phpcs --colors -nq --report="full" --extensions="php" $(git diff --name-only --diff-filter=ACMR origin/${{ github.base_ref }} HEAD) + phpcs --colors -nq --report="checkstyle" --extensions="php" $(git diff --name-only --diff-filter=ACMR origin/${{ github.base_ref }} HEAD) diff --git a/.github/workflows/code-quality-push.yaml b/.github/workflows/code-quality-push.yaml index 3de3f6a97..ff209d0b5 100644 --- a/.github/workflows/code-quality-push.yaml +++ b/.github/workflows/code-quality-push.yaml @@ -9,23 +9,16 @@ on: jobs: codeQuality: runs-on: ubuntu-latest - name: PHP + name: PHPCS steps: - name: Checkout changes uses: actions/checkout@v1 - - name: Install PHP - uses: shivammathur/setup-php@master + - name: Install PHP and PHP Code Sniffer + uses: shivammathur/setup-php@v1 with: - php-version: 7.2 - - name: Install Composer dependencies - run: composer install --no-interaction --no-progress --no-suggest - - name: Reset October modules and library - run: | - git reset --hard HEAD - rm -rf ./vendor/october/rain - wget https://github.com/octobercms/library/archive/develop.zip -O ./vendor/october/develop.zip - unzip ./vendor/october/develop.zip -d ./vendor/october - mv ./vendor/october/library-develop ./vendor/october/rain - composer dump-autoload + php-version: '7.3' + tools: phpcs + - name: Setup problem matcher for PHPCS + run: echo "::add-matcher::${{ github.workspace }}/.github/workflows/matchers/phpcs-matcher.json" - name: Run code quality checks - run: ./vendor/bin/phpcs --colors -nq --report="full" --extensions="php" $(git show --name-only --pretty="" --diff-filter=ACMR ${{ github.sha }}) + run: phpcs --colors -nq --report="checkstyle" --extensions="php" $(git show --name-only --pretty="" --diff-filter=ACMR ${{ github.sha }}) diff --git a/.github/workflows/frontend-tests.yaml b/.github/workflows/frontend-tests.yaml deleted file mode 100644 index 9d0068a0a..000000000 --- a/.github/workflows/frontend-tests.yaml +++ /dev/null @@ -1,24 +0,0 @@ -name: Tests - -on: - push: - branches: - - master - - develop - pull_request: - -jobs: - frontendTests: - runs-on: ubuntu-latest - name: JavaScript - steps: - - name: Checkout changes - uses: actions/checkout@v1 - - name: Install Node - uses: actions/setup-node@v1 - with: - node-version: 8 - - name: Install Node dependencies - run: npm install - - name: Run tests - run: npm run test diff --git a/.github/workflows/matchers/phpcs-matcher.json b/.github/workflows/matchers/phpcs-matcher.json new file mode 100644 index 000000000..5c80b26d2 --- /dev/null +++ b/.github/workflows/matchers/phpcs-matcher.json @@ -0,0 +1,23 @@ +{ + "problemMatcher": [ + { + "owner": "phpcs", + "severity": "error", + "pattern": [ + { + "regexp": "^$", + "file": 1 + }, + { + "regexp": "+)$", + "line": 1, + "column": 2, + "severity": 3, + "message": 4, + "code": 5, + "loop": true + } + ] + } + ] + } diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 072b795da..a27246d97 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,37 +8,56 @@ on: pull_request: jobs: + frontendTests: + runs-on: ubuntu-latest + name: JavaScript + steps: + - name: Checkout changes + uses: actions/checkout@v1 + - name: Install Node + uses: actions/setup-node@v1 + with: + node-version: 8 + - name: Install Node dependencies + run: npm install + - name: Run tests + run: npm run test phpUnitTests: runs-on: ubuntu-latest strategy: max-parallel: 6 matrix: - phpVersions: ['7.1', '7.2', '7.3', '7.4'] + phpVersions: ['7.2', '7.3', '7.4'] fail-fast: false - name: PHP ${{ matrix.phpVersions }} + name: Unit Tests / PHP ${{ matrix.phpVersions }} steps: - name: Checkout changes uses: actions/checkout@v1 - name: Install PHP - uses: shivammathur/setup-php@master + uses: shivammathur/setup-php@v1 with: php-version: ${{ matrix.phpVersions }} - extension-csv: mbstring, intl, gd, xml, sqlite + extensions: mbstring, intl, gd, xml, sqlite + - name: Setup problem matchers for PHPUnit + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + - name: Set Composer cache + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + - name: Cache Composer dependencies + uses: actions/cache@v1 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- - name: Install Composer dependencies run: composer install --no-interaction --no-progress --no-suggest --no-scripts - - name: Reset October modules and library + - name: Run post-update Composer scripts + run: php artisan package:discover + - name: Reset October modules run: | git reset --hard HEAD - rm -rf ./vendor/october/rain - wget https://github.com/octobercms/library/archive/develop.zip -O ./vendor/october/develop.zip - unzip ./vendor/october/develop.zip -d ./vendor/october - mv ./vendor/october/library-develop ./vendor/october/rain - composer dump-autoload - - name: Run post-update Composer scripts - run: | - php artisan october:util set build - php artisan package:discover + composer dumpautoload - name: Run Linting and Tests run: | ./vendor/bin/parallel-lint --exclude vendor --exclude storage --exclude tests/fixtures/plugins/testvendor/goto/Plugin.php . - ./vendor/bin/phpunit + ./vendor/bin/phpunit --prepend ./vendor/october/rain/src/Support/helpers.php diff --git a/.gitignore b/.gitignore index 4cd08cf9a..63a1b61d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,32 @@ -/bootstrap/compiled.php +# Composer ignores /vendor composer.phar -.DS_Store -.idea +composer.lock + +# Framework ignores .env .env.*.php .env.php +selenium.php +/bootstrap/compiled.php +.phpunit.result.cache + +# Hosting ignores php_errors.log nginx-error.log nginx-access.log nginx-ssl.access.log nginx-ssl.error.log -php-errors.log sftp-config.json .ftpconfig -selenium.php -composer.lock -package-lock.json -/node_modules + +# Editor ignores +nbproject +.idea +.vscode _ide_helper.php -# for netbeans -nbproject +# Other ignores +.DS_Store +package-lock.json +/node_modules diff --git a/artisan b/artisan index 961e94d0b..df630d0d6 100644 --- a/artisan +++ b/artisan @@ -28,7 +28,7 @@ $app = require_once __DIR__.'/bootstrap/app.php'; | */ -$kernel = $app->make('Illuminate\Contracts\Console\Kernel'); +$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class); $status = $kernel->handle( $input = new Symfony\Component\Console\Input\ArgvInput, @@ -48,4 +48,4 @@ $status = $kernel->handle( $kernel->terminate($input, $status); -exit($status); \ No newline at end of file +exit($status); diff --git a/bootstrap/autoload.php b/bootstrap/autoload.php index b980622d7..6533c54ca 100644 --- a/bootstrap/autoload.php +++ b/bootstrap/autoload.php @@ -35,20 +35,3 @@ require $helperPath; */ require __DIR__.'/../vendor/autoload.php'; - -/* -|-------------------------------------------------------------------------- -| Include The Compiled Class File -|-------------------------------------------------------------------------- -| -| To dramatically increase your application's performance, you may use a -| compiled class file which contains all of the classes commonly used -| by a request. The Artisan "optimize" is used to create this file. -| -*/ - -$compiledPath = __DIR__.'/../storage/framework/compiled.php'; - -if (file_exists($compiledPath)) { - require $compiledPath; -} diff --git a/composer.json b/composer.json index eb4f8fe37..9542fc5a1 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "october/october", - "description": "OctoberCMS", + "description": "October CMS", "homepage": "https://octobercms.com", "type": "project", "keywords": ["october", "cms", "octobercms", "laravel"], @@ -24,37 +24,34 @@ } ], "support": { + "paid": "https://octobercms.com/premium-support", "issues": "https://github.com/octobercms/october/issues", "forum": "https://octobercms.com/forum/", "docs": "https://octobercms.com/docs/", - "irc": "irc://irc.freenode.net/october", "source": "https://github.com/octobercms/october" }, "require": { - "php": ">=7.0.8", - "ext-mbstring": "*", - "ext-openssl": "*", - "october/rain": "~1.0", - "october/system": "~1.0", - "october/backend": "~1.0", - "october/cms": "~1.0", - "laravel/framework": "~5.5.40", + "php": ">=7.2", + "october/rain": "dev-develop as 1.0", + "october/system": "dev-develop", + "october/backend": "dev-develop", + "october/cms": "dev-develop", + "laravel/framework": "~6.0", "wikimedia/composer-merge-plugin": "1.4.1" }, "require-dev": { - "fzaninotto/faker": "~1.7", - "phpunit/phpunit": "~6.5", - "phpunit/phpunit-selenium": "~1.2", - "meyfa/phpunit-assert-gd": "1.1.0", + "phpunit/phpunit": "^8.0|^9.0", + "fzaninotto/faker": "~1.9", "squizlabs/php_codesniffer": "3.*", - "php-parallel-lint/php-parallel-lint": "^1.0" + "php-parallel-lint/php-parallel-lint": "^1.0", + "meyfa/phpunit-assert-gd": "^2.0.0", + "dms/phpunit-arraysubset-asserts": "^0.1.0" }, "autoload-dev": { "classmap": [ "tests/concerns/InteractsWithAuthentication.php", "tests/fixtures/backend/models/UserFixture.php", "tests/TestCase.php", - "tests/UiTestCase.php", "tests/PluginTestCase.php" ] }, @@ -66,12 +63,21 @@ "post-update-cmd": [ "php artisan october:util set build", "php artisan package:discover" + ], + "test": [ + "phpunit --stop-on-failure" + ], + "lint": [ + "parallel-lint --exclude vendor --exclude storage --exclude tests/fixtures/plugins/testvendor/goto/Plugin.php ." + ], + "sniff": [ + "phpcs --colors -nq --report=\"full\" --extensions=\"php\"" ] }, "config": { "preferred-install": "dist", "platform": { - "php": "7.0.8" + "php": "7.2" } }, "minimum-stability": "dev", diff --git a/config/app.php b/config/app.php index 23aad8473..56e4959ab 100644 --- a/config/app.php +++ b/config/app.php @@ -111,21 +111,6 @@ return [ 'cipher' => 'AES-256-CBC', - /* - |-------------------------------------------------------------------------- - | Logging Configuration - |-------------------------------------------------------------------------- - | - | Here you may configure the log settings for your application. Out of - | the box, Laravel uses the Monolog PHP logging library. This gives - | you a variety of powerful log handlers / formatters to utilize. - | - | Available Settings: "single", "daily", "syslog", "errorlog" - | - */ - - 'log' => 'single', - /* |-------------------------------------------------------------------------- | Autoloaded Service Providers @@ -144,6 +129,26 @@ return [ 'System\ServiceProvider', ]), + /* + |-------------------------------------------------------------------------- + | Load automatically discovered packages + |-------------------------------------------------------------------------- + | + | By default, October CMS disables the loading of discovered packages + | through Laravel's package discovery service, in order to allow packages + | used by plugins to be disabled if the plugin itself is disabled. + | + | Set this to `true` to enable automatic loading of these packages. This + | will result in packages being loaded, even if the plugin using them is + | disabled. This is NOT RECOMMENDED. + | + | Please note that packages defined in `app.providers` will still be loaded + | even if discovery is disabled. + | + */ + + 'loadDiscoveredPackages' => false, + /* |-------------------------------------------------------------------------- | Class Aliases diff --git a/config/database.php b/config/database.php index 70f1420c6..a06bc7586 100644 --- a/config/database.php +++ b/config/database.php @@ -116,6 +116,7 @@ return [ 'redis' => [ + 'client' => 'predis', 'cluster' => false, 'default' => [ diff --git a/config/develop.php b/config/develop.php index cd4aee7d7..2c1cbc642 100644 --- a/config/develop.php +++ b/config/develop.php @@ -20,5 +20,27 @@ return [ */ 'decompileBackendAssets' => false, + + /* + |-------------------------------------------------------------------------- + | Allow deep-level symlinks + |-------------------------------------------------------------------------- + | + | October CMS, by default, will allow symlinks within the first level of + | subdirectories. When this feature is enabled, the system will allow + | symlinks to be used at any directory level. This can be useful for + | symlinking individual plugins or themes. + | + | Please note that this has a negative effect on performance. This feature + | abides by "cms.restrictBaseDir" - if enabled, symlinks cannot point to + | resources outside of the root folder. + | + | true - allow symlinks at any level + | + | false - only allow symlinks at the first level of subdirectories (default) + | + */ + + 'allowDeepSymlinks' => false, ]; diff --git a/config/filesystems.php b/config/filesystems.php index 4d843013c..36d9a0dfa 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -11,7 +11,7 @@ return [ | by the framework. A "local" driver, as well as a variety of cloud | based drivers are available for your choosing. Just store away! | - | Supported: "local", "s3", "rackspace" + | Supported: "local", "ftp", "sftp", "s3", "rackspace" | */ diff --git a/config/hashing.php b/config/hashing.php new file mode 100644 index 000000000..842577087 --- /dev/null +++ b/config/hashing.php @@ -0,0 +1,52 @@ + 'bcrypt', + + /* + |-------------------------------------------------------------------------- + | Bcrypt Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Bcrypt algorithm. This will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'bcrypt' => [ + 'rounds' => env('BCRYPT_ROUNDS', 10), + ], + + /* + |-------------------------------------------------------------------------- + | Argon Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Argon algorithm. These will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'argon' => [ + 'memory' => 1024, + 'threads' => 2, + 'time' => 2, + ], + +]; diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 000000000..900d48123 --- /dev/null +++ b/config/logging.php @@ -0,0 +1,91 @@ + env('LOG_CHANNEL', 'single'), + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Out of + | the box, Laravel uses the Monolog PHP logging library. This gives + | you a variety of powerful log handlers / formatters to utilize. + | + | Available Drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", + | "custom", "stack" + | + */ + + 'channels' => [ + 'stack' => [ + 'driver' => 'stack', + 'channels' => ['daily'], + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/system.log'), + 'level' => 'debug', + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/system.log'), + 'level' => 'debug', + 'days' => 14, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => 'October CMS Log', + 'emoji' => ':boom:', + 'level' => 'critical', + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => 'debug', + 'handler' => \Monolog\Handler\SyslogUdpHandler::class, + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + ], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'handler' => \Monolog\Handler\StreamHandler::class, + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'with' => [ + 'stream' => 'php://stderr', + ], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => 'debug', + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => 'debug', + ], + ], + +]; diff --git a/config/mail.php b/config/mail.php index 7c2332d2f..4c471d568 100644 --- a/config/mail.php +++ b/config/mail.php @@ -12,7 +12,7 @@ return [ | your application here. By default, Laravel is setup for SMTP mail. | | Supported: "smtp", "sendmail", "mailgun", "mandrill", "ses", - | "sparkpost", "log", "array" + | "postmark", "sparkpost", "log", "array" | */ diff --git a/config/services.php b/config/services.php index c2d453065..d53643b03 100644 --- a/config/services.php +++ b/config/services.php @@ -24,6 +24,10 @@ return [ 'secret' => '', ], + 'postmark' => [ + 'token' => '', + ], + 'ses' => [ 'key' => '', 'secret' => '', diff --git a/index.php b/index.php index ba43df3ec..9c4d23a06 100644 --- a/index.php +++ b/index.php @@ -37,7 +37,7 @@ $app = require_once __DIR__.'/bootstrap/app.php'; | */ -$kernel = $app->make('Illuminate\Contracts\Http\Kernel'); +$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); $response = $kernel->handle( $request = Illuminate\Http\Request::capture() diff --git a/modules/backend/ServiceProvider.php b/modules/backend/ServiceProvider.php index b0a5026f8..4c3825dd0 100644 --- a/modules/backend/ServiceProvider.php +++ b/modules/backend/ServiceProvider.php @@ -80,6 +80,7 @@ class ServiceProvider extends ModuleServiceProvider $combiner->registerBundle('~/modules/backend/formwidgets/colorpicker/assets/less/colorpicker.less'); $combiner->registerBundle('~/modules/backend/formwidgets/permissioneditor/assets/less/permissioneditor.less'); $combiner->registerBundle('~/modules/backend/formwidgets/markdowneditor/assets/less/markdowneditor.less'); + $combiner->registerBundle('~/modules/backend/formwidgets/sensitive/assets/less/sensitive.less'); /* * Rich Editor is protected by DRM @@ -199,6 +200,7 @@ class ServiceProvider extends ModuleServiceProvider $manager->registerFormWidget('Backend\FormWidgets\TagList', 'taglist'); $manager->registerFormWidget('Backend\FormWidgets\MediaFinder', 'mediafinder'); $manager->registerFormWidget('Backend\FormWidgets\NestedForm', 'nestedform'); + $manager->registerFormWidget('Backend\FormWidgets\Sensitive', 'sensitive'); }); } diff --git a/modules/backend/behaviors/ImportExportController.php b/modules/backend/behaviors/ImportExportController.php index c146050b6..b0278ccdc 100644 --- a/modules/backend/behaviors/ImportExportController.php +++ b/modules/backend/behaviors/ImportExportController.php @@ -11,7 +11,7 @@ use Backend\Behaviors\ImportExportController\TranscodeFilter; use Illuminate\Database\Eloquent\MassAssignmentException; use League\Csv\Reader as CsvReader; use League\Csv\Writer as CsvWriter; -use October\Rain\Parse\League\EscapeFormula as CsvEscapeFormula; +use League\Csv\EscapeFormula as CsvEscapeFormula; use ApplicationException; use SplTempFileObject; use Exception; @@ -624,9 +624,7 @@ class ImportExportController extends ControllerBehavior $csv->setDelimiter($options['delimiter']); $csv->setEnclosure($options['enclosure']); $csv->setEscape($options['escape']); - - // Temporary until upgrading to league/csv >= 9.1.0 (will be $csv->addFormatter($formatter)) - $formatter = new CsvEscapeFormula(); + $csv->addFormatter(new CsvEscapeFormula()); /* * Add headers @@ -662,9 +660,6 @@ class ImportExportController extends ControllerBehavior $record[] = $value; } - // Temporary until upgrading to league/csv >= 9.1.0 - $record = $formatter($record); - $csv->insertOne($record); } diff --git a/modules/backend/classes/FormField.php b/modules/backend/classes/FormField.php index f2b6efc68..d9782a267 100644 --- a/modules/backend/classes/FormField.php +++ b/modules/backend/classes/FormField.php @@ -124,7 +124,7 @@ class FormField /** * @var string Specifies a comment to accompany the field */ - public $comment; + public $comment = ''; /** * @var string Specifies the comment position. @@ -139,7 +139,7 @@ class FormField /** * @var string Specifies a message to display when there is no value supplied (placeholder). */ - public $placeholder; + public $placeholder = ''; /** * @var array Contains a list of attributes specified in the field configuration. diff --git a/modules/backend/composer.json b/modules/backend/composer.json index 15cfd2a3f..5d4231af2 100644 --- a/modules/backend/composer.json +++ b/modules/backend/composer.json @@ -8,11 +8,13 @@ "authors": [ { "name": "Alexey Bobkov", - "email": "aleksey.bobkov@gmail.com" + "email": "aleksey.bobkov@gmail.com", + "role": "Co-founder" }, { "name": "Samuel Georges", - "email": "daftspunky@gmail.com" + "email": "daftspunky@gmail.com", + "role": "Co-founder" }, { "name": "Luke Towers", @@ -22,9 +24,10 @@ } ], "require": { - "php": ">=7.0", + "php": ">=7.2", "composer/installers": "~1.0", - "october/rain": "~1.0" + "october/rain": "~1.0", + "laravel/framework": "~6.0" }, "autoload": { "psr-4": { diff --git a/modules/backend/database/seeds/DatabaseSeeder.php b/modules/backend/database/seeds/DatabaseSeeder.php index 7be5f9885..78c561441 100644 --- a/modules/backend/database/seeds/DatabaseSeeder.php +++ b/modules/backend/database/seeds/DatabaseSeeder.php @@ -12,8 +12,8 @@ class DatabaseSeeder extends Seeder */ public function run() { - Eloquent::unguard(); - - $this->call('Backend\Database\Seeds\SeedSetupAdmin'); + Eloquent::unguarded(function () { + $this->call('Backend\Database\Seeds\SeedSetupAdmin'); + }); } } diff --git a/modules/backend/formwidgets/Sensitive.php b/modules/backend/formwidgets/Sensitive.php new file mode 100644 index 000000000..a28a8d60b --- /dev/null +++ b/modules/backend/formwidgets/Sensitive.php @@ -0,0 +1,117 @@ +fillFromConfig([ + 'readOnly', + 'disabled', + 'allowCopy', + 'hiddenPlaceholder', + 'hideOnTabChange', + ]); + + if ($this->formField->disabled || $this->formField->readOnly) { + $this->previewMode = true; + } + } + + /** + * @inheritDoc + */ + public function render() + { + $this->prepareVars(); + + return $this->makePartial('sensitive'); + } + + /** + * Prepares the view data for the widget partial. + */ + public function prepareVars() + { + $this->vars['readOnly'] = $this->readOnly; + $this->vars['disabled'] = $this->disabled; + $this->vars['hasValue'] = !empty($this->getLoadValue()); + $this->vars['allowCopy'] = $this->allowCopy; + $this->vars['hiddenPlaceholder'] = $this->hiddenPlaceholder; + $this->vars['hideOnTabChange'] = $this->hideOnTabChange; + } + + /** + * Reveals the value of a hidden, unmodified sensitive field. + * + * @return array + */ + public function onShowValue() + { + return [ + 'value' => $this->getLoadValue() + ]; + } + + /** + * @inheritDoc + */ + public function getSaveValue($value) + { + if ($value === $this->hiddenPlaceholder) { + $value = $this->getLoadValue(); + } + + return $value; + } + + /** + * @inheritDoc + */ + protected function loadAssets() + { + $this->addCss('css/sensitive.css', 'core'); + $this->addJs('js/sensitive.js', 'core'); + } +} diff --git a/modules/backend/formwidgets/sensitive/assets/css/sensitive.css b/modules/backend/formwidgets/sensitive/assets/css/sensitive.css new file mode 100644 index 000000000..c8ac9378a --- /dev/null +++ b/modules/backend/formwidgets/sensitive/assets/css/sensitive.css @@ -0,0 +1,2 @@ +div[data-control="sensitive"] a[data-toggle], +div[data-control="sensitive"] a[data-copy] {box-shadow:none;border:1px solid #d1d6d9;border-left:0} \ No newline at end of file diff --git a/modules/backend/formwidgets/sensitive/assets/js/sensitive.js b/modules/backend/formwidgets/sensitive/assets/js/sensitive.js new file mode 100644 index 000000000..69251304c --- /dev/null +++ b/modules/backend/formwidgets/sensitive/assets/js/sensitive.js @@ -0,0 +1,192 @@ +/* + * Sensitive field widget plugin. + * + * Data attributes: + * - data-control="sensitive" - enables the plugin on an element + * + * JavaScript API: + * $('div#someElement').sensitive({...}) + */ ++function ($) { "use strict"; + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + var Sensitive = function(element, options) { + this.$el = $(element) + this.options = options + this.clean = Boolean(this.$el.data('clean')) + this.hidden = true + + this.$input = this.$el.find('[data-input]').first() + this.$toggle = this.$el.find('[data-toggle]').first() + this.$icon = this.$el.find('[data-icon]').first() + this.$loader = this.$el.find('[data-loader]').first() + this.$copy = this.$el.find('[data-copy]').first() + + $.oc.foundation.controlUtils.markDisposable(element) + Base.call(this) + this.init() + } + + Sensitive.DEFAULTS = { + readOnly: false, + disabled: false, + eventHandler: null, + hideOnTabChange: false, + } + + Sensitive.prototype = Object.create(BaseProto) + Sensitive.prototype.constructor = Sensitive + + Sensitive.prototype.init = function() { + this.$input.on('keydown', this.proxy(this.onInput)) + this.$toggle.on('click', this.proxy(this.onToggle)) + + if (this.options.hideOnTabChange) { + // Watch for tab change or minimise + document.addEventListener('visibilitychange', this.proxy(this.onTabChange)) + } + + if (this.$copy.length) { + this.$copy.on('click', this.proxy(this.onCopy)) + } + } + + Sensitive.prototype.dispose = function () { + this.$input.off('keydown', this.proxy(this.onInput)) + this.$toggle.off('click', this.proxy(this.onToggle)) + + if (this.options.hideOnTabChange) { + document.removeEventListener('visibilitychange', this.proxy(this.onTabChange)) + } + + if (this.$copy.length) { + this.$copy.off('click', this.proxy(this.onCopy)) + } + + this.$input = this.$toggle = this.$icon = this.$loader = null + this.$el = null + + BaseProto.dispose.call(this) + } + + Sensitive.prototype.onInput = function() { + if (this.clean) { + this.clean = false + this.$input.val('') + } + + return true + } + + Sensitive.prototype.onToggle = function() { + if (this.$input.val() !== '' && this.clean) { + this.reveal() + } else { + this.toggleVisibility() + } + + return true + } + + Sensitive.prototype.onTabChange = function() { + if (document.hidden && !this.hidden) { + this.toggleVisibility() + } + } + + Sensitive.prototype.onCopy = function() { + var that = this, + deferred = $.Deferred(), + isHidden = this.hidden + + deferred.then(function () { + if (that.hidden) { + that.toggleVisibility() + } + + that.$input.focus() + that.$input.select() + + try { + document.execCommand('copy') + } catch (err) { + } + + that.$input.blur() + if (isHidden) { + that.toggleVisibility() + } + }) + + if (this.$input.val() !== '' && this.clean) { + this.reveal(deferred) + } else { + deferred.resolve() + } + } + + Sensitive.prototype.toggleVisibility = function() { + if (this.hidden) { + this.$input.attr('type', 'text') + } else { + this.$input.attr('type', 'password') + } + + this.$icon.toggleClass('icon-eye icon-eye-slash') + + this.hidden = !this.hidden + } + + Sensitive.prototype.reveal = function(deferred) { + var that = this + this.$icon.css({ + visibility: 'hidden' + }) + this.$loader.removeClass('hide') + + this.$input.request(this.options.eventHandler, { + success: function (data) { + that.$input.val(data.value) + that.clean = false + + that.$icon.css({ + visibility: 'visible' + }) + that.$loader.addClass('hide') + + that.toggleVisibility() + + if (deferred) { + deferred.resolve() + } + } + }) + } + + var old = $.fn.sensitive + + $.fn.sensitive = function (option) { + var args = Array.prototype.slice.call(arguments, 1), result + this.each(function () { + var $this = $(this) + var data = $this.data('oc.sensitive') + var options = $.extend({}, Sensitive.DEFAULTS, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('oc.sensitive', (data = new Sensitive(this, options))) + if (typeof option == 'string') result = data[option].apply(data, args) + if (typeof result != 'undefined') return false + }) + + return result ? result : this + } + + $.fn.sensitive.noConflict = function () { + $.fn.sensitive = old + return this + } + + $(document).render(function () { + $('[data-control="sensitive"]').sensitive() + }); + +}(window.jQuery); diff --git a/modules/backend/formwidgets/sensitive/assets/less/sensitive.less b/modules/backend/formwidgets/sensitive/assets/less/sensitive.less new file mode 100644 index 000000000..5717658f2 --- /dev/null +++ b/modules/backend/formwidgets/sensitive/assets/less/sensitive.less @@ -0,0 +1,10 @@ +@import "../../../../assets/less/core/boot.less"; + +div[data-control="sensitive"] { + a[data-toggle], + a[data-copy] { + box-shadow: none; + border: 1px solid @input-group-addon-border-color; + border-left: 0; + } +} diff --git a/modules/backend/formwidgets/sensitive/partials/_sensitive.htm b/modules/backend/formwidgets/sensitive/partials/_sensitive.htm new file mode 100644 index 000000000..b913070d7 --- /dev/null +++ b/modules/backend/formwidgets/sensitive/partials/_sensitive.htm @@ -0,0 +1,41 @@ +
data-hide-on-tab-change="true" +> +
+
+ previewMode): ?>disabled="disabled" + autocomplete="off" + data-input + /> + + + + + + + + +
+
+ +
+
+
diff --git a/modules/backend/models/ExportModel.php b/modules/backend/models/ExportModel.php index 1a03a813b..e6a767e80 100644 --- a/modules/backend/models/ExportModel.php +++ b/modules/backend/models/ExportModel.php @@ -5,7 +5,7 @@ use Lang; use Model; use Response; use League\Csv\Writer as CsvWriter; -use October\Rain\Parse\League\EscapeFormula as CsvEscapeFormula; +use League\Csv\EscapeFormula as CsvEscapeFormula; use ApplicationException; use SplTempFileObject; @@ -112,8 +112,7 @@ abstract class ExportModel extends Model $csv->setEscape($options['escape']); } - // Temporary until upgrading to league/csv >= 9.1.0 (will be $csv->addFormatter($formatter)) - $formatter = new CsvEscapeFormula(); + $csv->addFormatter(new CsvEscapeFormula()); /* * Add headers @@ -128,10 +127,6 @@ abstract class ExportModel extends Model */ foreach ($results as $result) { $data = $this->matchDataToColumns($result, $columns); - - // Temporary until upgrading to league/csv >= 9.1.0 - $data = $formatter($data); - $csv->insertOne($data); } diff --git a/modules/backend/models/ImportModel.php b/modules/backend/models/ImportModel.php index 12f937a16..09c1bf426 100644 --- a/modules/backend/models/ImportModel.php +++ b/modules/backend/models/ImportModel.php @@ -5,6 +5,7 @@ use Str; use Lang; use Model; use League\Csv\Reader as CsvReader; +use League\Csv\Statement as CsvStatement; /** * Model used for importing data @@ -108,11 +109,6 @@ abstract class ImportModel extends Model */ $reader = CsvReader::createFromPath($filePath, 'r'); - // Filter out empty rows - $reader->addFilter(function (array $row) { - return count($row) > 1 || reset($row) !== null; - }); - if ($options['delimiter'] !== null) { $reader->setDelimiter($options['delimiter']); } @@ -125,15 +121,11 @@ abstract class ImportModel extends Model $reader->setEscape($options['escape']); } - if ($options['firstRowTitles']) { - $reader->setOffset(1); - } - if ( $options['encoding'] !== null && - $reader->isActiveStreamFilter() + $reader->supportsStreamFilter() ) { - $reader->appendStreamFilter(sprintf( + $reader->addStreamFilter(sprintf( '%s%s:%s', TranscodeFilter::FILTER_NAME, strtolower($options['encoding']), @@ -141,8 +133,19 @@ abstract class ImportModel extends Model )); } + // Create reader statement + $stmt = (new CsvStatement) + ->where(function (array $row) { + // Filter out empty rows + return count($row) > 1 || reset($row) !== null; + }); + + if ($options['firstRowTitles']) { + $stmt = $stmt->offset(1); + } + $result = []; - $contents = $reader->fetch(); + $contents = $stmt->process($reader); foreach ($contents as $row) { $result[] = $this->processImportRow($row, $matches); } diff --git a/modules/backend/models/User.php b/modules/backend/models/User.php index 650a1378e..46259792b 100644 --- a/modules/backend/models/User.php +++ b/modules/backend/models/User.php @@ -27,8 +27,8 @@ class User extends UserBase public $rules = [ 'email' => 'required|between:6,255|email|unique:backend_users', 'login' => 'required|between:2,255|unique:backend_users', - 'password' => 'required:create|between:4,255|confirmed', - 'password_confirmation' => 'required_with:password|between:4,255' + 'password' => 'required:create|min:4|confirmed', + 'password_confirmation' => 'required_with:password|min:4' ]; /** diff --git a/modules/backend/routes.php b/modules/backend/routes.php index 81904802f..0268f7efc 100644 --- a/modules/backend/routes.php +++ b/modules/backend/routes.php @@ -25,7 +25,7 @@ App::before(function ($request) { 'middleware' => ['web'], 'prefix' => Config::get('cms.backendUri', 'backend') ], function () { - Route::any('{slug}', 'Backend\Classes\BackendController@run')->where('slug', '(.*)?'); + Route::any('{slug?}', 'Backend\Classes\BackendController@run')->where('slug', '(.*)?'); }) ; diff --git a/modules/cms/classes/Asset.php b/modules/cms/classes/Asset.php index 12b79ea35..0bfc2eba8 100644 --- a/modules/cms/classes/Asset.php +++ b/modules/cms/classes/Asset.php @@ -287,25 +287,14 @@ class Asset extends Extendable $directory = $this->theme->getPath() . '/' . $this->dirName . '/'; $filePath = $directory . $fileName; - $path = realpath($filePath); - - /** - * If the path doesn't exist yet, then create it temporarily - * in order to run realpath() resolution on it to verify the - * final destination and then remove the temporary file. - */ - if (!$path) { - touch($filePath); - $path = realpath($filePath); - unlink($filePath); - } + $resolvedPath = resolve_path($filePath); // Limit paths to those under the theme's assets directory - if (!starts_with($path, $directory)) { + if (!starts_with($resolvedPath, $directory)) { return false; } - return $path; + return $resolvedPath; } /** diff --git a/modules/cms/classes/CmsCompoundObject.php b/modules/cms/classes/CmsCompoundObject.php index 5705515bc..fca40ff67 100644 --- a/modules/cms/classes/CmsCompoundObject.php +++ b/modules/cms/classes/CmsCompoundObject.php @@ -316,7 +316,8 @@ class CmsCompoundObject extends CmsObject self::$objectComponentPropertyMap = $objectComponentMap; - Cache::put($key, base64_encode(serialize($objectComponentMap)), Config::get('cms.parsedPageCacheTTL', 10)); + $expiresAt = now()->addMinutes(Config::get('cms.parsedPageCacheTTL', 10)); + Cache::put($key, base64_encode(serialize($objectComponentMap)), $expiresAt); if (array_key_exists($componentName, $objectComponentMap[$objectCode])) { return $objectComponentMap[$objectCode][$componentName]; diff --git a/modules/cms/classes/CmsObject.php b/modules/cms/classes/CmsObject.php index 798a065a0..714650150 100644 --- a/modules/cms/classes/CmsObject.php +++ b/modules/cms/classes/CmsObject.php @@ -227,7 +227,16 @@ class CmsObject extends HalcyonModel implements CmsObjectContract $fileName = $this->fileName; } - return $this->theme->getPath().'/'.$this->getObjectTypeDirName().'/'.$fileName; + $directory = $this->theme->getPath() . '/' . $this->getObjectTypeDirName() . '/'; + $filePath = $directory . $fileName; + $resolvedPath = resolve_path($filePath); + + // Limit paths to those under the corresponding theme directory + if (!starts_with($resolvedPath, $directory)) { + return false; + } + + return $resolvedPath; } /** diff --git a/modules/cms/classes/CmsObjectCollection.php b/modules/cms/classes/CmsObjectCollection.php index ffc7afc73..2105789ac 100644 --- a/modules/cms/classes/CmsObjectCollection.php +++ b/modules/cms/classes/CmsObjectCollection.php @@ -1,5 +1,6 @@ filter(function ($object) use ($property, $value, $strict) { + if (empty($value) || !is_string($value)) { + throw new ApplicationException('You must provide a string value to compare with when executing a "where" ' + . 'query for CMS object collections.'); + } + if (!isset($strict) || !is_bool($strict)) { + $strict = true; + } + + return $this->filter(function ($object) use ($property, $value, $strict) { if (!array_key_exists($property, $object->settings)) { return false; } diff --git a/modules/cms/classes/CodeParser.php b/modules/cms/classes/CodeParser.php index dd6687637..b747a20e5 100644 --- a/modules/cms/classes/CodeParser.php +++ b/modules/cms/classes/CodeParser.php @@ -224,7 +224,8 @@ class CodeParser $cached = $this->getCachedInfo() ?: []; $cached[$this->filePath] = $cacheItem; - Cache::put($this->dataCacheKey, base64_encode(serialize($cached)), 1440); + $expiresAt = now()->addMinutes(1440); + Cache::put($this->dataCacheKey, base64_encode(serialize($cached)), $expiresAt); self::$cache[$this->filePath] = $result; } diff --git a/modules/cms/classes/Router.php b/modules/cms/classes/Router.php index 0c033fd81..4f75656f1 100644 --- a/modules/cms/classes/Router.php +++ b/modules/cms/classes/Router.php @@ -127,10 +127,11 @@ class Router : $fileName; $key = $this->getUrlListCacheKey(); + $expiresAt = now()->addMinutes(Config::get('cms.urlCacheTtl', 1)); Cache::put( $key, base64_encode(serialize($urlList)), - Config::get('cms.urlCacheTtl', 1) + $expiresAt ); } } @@ -251,7 +252,8 @@ class Router $this->urlMap = $map; if ($cacheable) { - Cache::put($key, base64_encode(serialize($map)), Config::get('cms.urlCacheTtl', 1)); + $expiresAt = now()->addMinutes(Config::get('cms.urlCacheTtl', 1)); + Cache::put($key, base64_encode(serialize($map)), $expiresAt); } return false; diff --git a/modules/cms/classes/Theme.php b/modules/cms/classes/Theme.php index a5306c897..6ce06a1c5 100644 --- a/modules/cms/classes/Theme.php +++ b/modules/cms/classes/Theme.php @@ -158,7 +158,8 @@ class Theme if ($checkDatabase && App::hasDatabase()) { try { try { - $dbResult = Cache::remember(self::ACTIVE_KEY, 1440, function () { + $expiresAt = now()->addMinutes(1440); + $dbResult = Cache::remember(self::ACTIVE_KEY, $expiresAt, function () { return Parameter::applyKey(self::ACTIVE_KEY)->value('value'); }); } diff --git a/modules/cms/composer.json b/modules/cms/composer.json index 49e2d944b..9e4cfa70e 100644 --- a/modules/cms/composer.json +++ b/modules/cms/composer.json @@ -8,11 +8,13 @@ "authors": [ { "name": "Alexey Bobkov", - "email": "aleksey.bobkov@gmail.com" + "email": "aleksey.bobkov@gmail.com", + "role": "Co-founder" }, { "name": "Samuel Georges", - "email": "daftspunky@gmail.com" + "email": "daftspunky@gmail.com", + "role": "Co-founder" }, { "name": "Luke Towers", @@ -22,9 +24,10 @@ } ], "require": { - "php": ">=7.0", + "php": ">=7.2", "composer/installers": "~1.0", - "october/rain": "~1.0" + "october/rain": "~1.0", + "laravel/framework": "~6.0" }, "autoload": { "psr-4": { diff --git a/modules/cms/routes.php b/modules/cms/routes.php index 76f35c4b3..1aaf5ec5d 100644 --- a/modules/cms/routes.php +++ b/modules/cms/routes.php @@ -22,7 +22,7 @@ App::before(function ($request) { * The CMS module intercepts all URLs that were not * handled by the back-end modules. */ - Route::any('{slug}', 'Cms\Classes\CmsController@run')->where('slug', '(.*)?')->middleware('web'); + Route::any('{slug?}', 'Cms\Classes\CmsController@run')->where('slug', '(.*)?')->middleware('web'); /** * @event cms.route diff --git a/modules/cms/traits/UrlMaker.php b/modules/cms/traits/UrlMaker.php index 210922cfa..ebe0c050f 100644 --- a/modules/cms/traits/UrlMaker.php +++ b/modules/cms/traits/UrlMaker.php @@ -190,7 +190,8 @@ trait UrlMaker 'mtime' => @File::lastModified($filePath) ]; - Cache::put($key, serialize($cached), Config::get('cms.parsedPageCacheTTL', 1440)); + $expiresAt = now()->addMinutes(Config::get('cms.parsedPageCacheTTL', 1440)); + Cache::put($key, serialize($cached), $expiresAt); return static::$urlPageName = $baseFileName; } diff --git a/modules/cms/twig/DebugExtension.php b/modules/cms/twig/DebugExtension.php index 242eb23a5..f974bb8e2 100644 --- a/modules/cms/twig/DebugExtension.php +++ b/modules/cms/twig/DebugExtension.php @@ -8,7 +8,7 @@ use Cms\Classes\Controller; use Cms\Classes\ComponentBase; use Illuminate\Pagination\Paginator; use Illuminate\Support\Collection; -use Illuminate\Support\Debug\HtmlDumper; +use Symfony\Component\VarDumper\Dumper\HtmlDumper; use Symfony\Component\VarDumper\Cloner\VarCloner; use October\Rain\Database\Model; diff --git a/modules/system/ServiceProvider.php b/modules/system/ServiceProvider.php index 9fc858f39..eb5b6c613 100644 --- a/modules/system/ServiceProvider.php +++ b/modules/system/ServiceProvider.php @@ -94,6 +94,7 @@ class ServiceProvider extends ModuleServiceProvider } } + Paginator::useBootstrapThree(); Paginator::defaultSimpleView('system::pagination.simple-default'); /* diff --git a/modules/system/aliases.php b/modules/system/aliases.php index bcb2c78b2..d6364cd13 100644 --- a/modules/system/aliases.php +++ b/modules/system/aliases.php @@ -16,7 +16,6 @@ return [ 'Eloquent' => Illuminate\Database\Eloquent\Model::class, 'Event' => Illuminate\Support\Facades\Event::class, 'Hash' => Illuminate\Support\Facades\Hash::class, - 'Input' => Illuminate\Support\Facades\Input::class, 'Lang' => Illuminate\Support\Facades\Lang::class, 'Log' => Illuminate\Support\Facades\Log::class, 'Mail' => Illuminate\Support\Facades\Mail::class, @@ -30,7 +29,6 @@ return [ 'Storage' => Illuminate\Support\Facades\Storage::class, 'Url' => Illuminate\Support\Facades\URL::class, // Preferred 'URL' => Illuminate\Support\Facades\URL::class, - 'Validator' => Illuminate\Support\Facades\Validator::class, 'View' => Illuminate\Support\Facades\View::class, /* @@ -42,6 +40,7 @@ return [ 'Config' => October\Rain\Support\Facades\Config::class, 'Seeder' => October\Rain\Database\Updates\Seeder::class, 'Flash' => October\Rain\Support\Facades\Flash::class, + 'Input' => October\Rain\Support\Facades\Input::class, 'Form' => October\Rain\Support\Facades\Form::class, 'Html' => October\Rain\Support\Facades\Html::class, 'Http' => October\Rain\Support\Facades\Http::class, @@ -52,6 +51,7 @@ return [ 'Twig' => October\Rain\Support\Facades\Twig::class, 'DbDongle' => October\Rain\Support\Facades\DbDongle::class, 'Schema' => October\Rain\Support\Facades\Schema::class, + 'Validator' => October\Rain\Support\Facades\Validator::class, 'Cms' => Cms\Facades\Cms::class, 'Backend' => Backend\Facades\Backend::class, 'BackendMenu' => Backend\Facades\BackendMenu::class, @@ -60,4 +60,12 @@ return [ 'SystemException' => October\Rain\Exception\SystemException::class, 'ApplicationException' => October\Rain\Exception\ApplicationException::class, 'ValidationException' => October\Rain\Exception\ValidationException::class, + + /* + * Fallback aliases + */ + // Input facade was removed in Laravel 6 - we are keeping it in the Rain library for backwards compatibility. + 'Illuminate\Support\Facades\Input' => October\Rain\Support\Facades\Input::class, + // Illuminate's HtmlDumper was "dumped" in Laravel 6 - we'll route this to Symfony's HtmlDumper as Laravel have done. + 'Illuminate\Support\Debug\HtmlDumper' => Symfony\Component\VarDumper\Dumper\HtmlDumper::class, ]; diff --git a/modules/system/classes/CombineAssets.php b/modules/system/classes/CombineAssets.php index 50a03c292..17ec7c2bd 100644 --- a/modules/system/classes/CombineAssets.php +++ b/modules/system/classes/CombineAssets.php @@ -10,11 +10,11 @@ use Route; use Config; use Request; use Response; -use Assetic\Asset\FileAsset; -use Assetic\Asset\AssetCache; -use Assetic\Asset\AssetCollection; -use Assetic\Factory\AssetFactory; -use October\Rain\Parse\Assetic\FilesystemCache; +use October\Rain\Assetic\Asset\FileAsset; +use October\Rain\Assetic\Asset\AssetCache; +use October\Rain\Assetic\Asset\AssetCollection; +use October\Rain\Assetic\Cache\FilesystemCache; +use October\Rain\Assetic\Factory\AssetFactory; use System\Helpers\Cache as CacheHelper; use ApplicationException; use DateTime; @@ -126,22 +126,22 @@ class CombineAssets /* * Register JavaScript filters */ - $this->registerFilter('js', new \October\Rain\Parse\Assetic\JavascriptImporter); + $this->registerFilter('js', new \October\Rain\Assetic\Filter\JavascriptImporter); /* * Register CSS filters */ - $this->registerFilter('css', new \Assetic\Filter\CssImportFilter); - $this->registerFilter(['css', 'less', 'scss'], new \Assetic\Filter\CssRewriteFilter); - $this->registerFilter('less', new \October\Rain\Parse\Assetic\LessCompiler); - $this->registerFilter('scss', new \October\Rain\Parse\Assetic\ScssCompiler); + $this->registerFilter('css', new \October\Rain\Assetic\Filter\CssImportFilter); + $this->registerFilter(['css', 'less', 'scss'], new \October\Rain\Assetic\Filter\CssRewriteFilter); + $this->registerFilter('less', new \October\Rain\Assetic\Filter\LessCompiler); + $this->registerFilter('scss', new \October\Rain\Assetic\Filter\ScssCompiler); /* * Minification filters */ if ($this->useMinify) { - $this->registerFilter('js', new \Assetic\Filter\JSMinFilter); - $this->registerFilter(['css', 'less', 'scss'], new \October\Rain\Parse\Assetic\StylesheetMinify); + $this->registerFilter('js', new \October\Rain\Assetic\Filter\JSMinFilter); + $this->registerFilter(['css', 'less', 'scss'], new \October\Rain\Assetic\Filter\StylesheetMinify); } /* diff --git a/modules/system/classes/MediaLibrary.php b/modules/system/classes/MediaLibrary.php index 50a6cc8f9..40808854e 100644 --- a/modules/system/classes/MediaLibrary.php +++ b/modules/system/classes/MediaLibrary.php @@ -134,10 +134,11 @@ class MediaLibrary $folderContents = $this->scanFolderContents($fullFolderPath); $cached[$fullFolderPath] = $folderContents; + $expiresAt = now()->addMinutes(Config::get('cms.storage.media.ttl', 10)); Cache::put( $this->cacheKey, base64_encode(serialize($cached)), - Config::get('cms.storage.media.ttl', 10) + $expiresAt ); } diff --git a/modules/system/classes/UpdateManager.php b/modules/system/classes/UpdateManager.php index 7e4779de1..1b9c3618f 100644 --- a/modules/system/classes/UpdateManager.php +++ b/modules/system/classes/UpdateManager.php @@ -29,11 +29,6 @@ class UpdateManager { use \October\Rain\Support\Traits\Singleton; - /** - * @var array The notes for the current operation. - */ - protected $notes = []; - /** * @var \Illuminate\Console\OutputStyle */ @@ -345,13 +340,13 @@ class UpdateManager /* * Rollback modules */ + if (isset($this->notesOutput)) { + $this->migrator->setOutput($this->notesOutput); + } + while (true) { $rolledBack = $this->migrator->rollback($paths, ['pretend' => false]); - foreach ($this->migrator->getNotes() as $note) { - $this->note($note); - } - if (count($rolledBack) == 0) { break; } @@ -403,13 +398,13 @@ class UpdateManager */ public function migrateModule($module) { - $this->migrator->run(base_path() . '/modules/' . strtolower($module) . '/database/migrations'); + if (isset($this->notesOutput)) { + $this->migrator->setOutput($this->notesOutput); + } $this->note($module); - foreach ($this->migrator->getNotes() as $note) { - $this->note(' - ' . $note); - } + $this->migrator->run(base_path() . '/modules/'.strtolower($module).'/database/migrations'); return $this; } @@ -518,13 +513,9 @@ class UpdateManager $this->note($name); - $this->versionManager->resetNotes()->setNotesOutput($this->notesOutput); + $this->versionManager->setNotesOutput($this->notesOutput); - if ($this->versionManager->updatePlugin($plugin) !== false) { - foreach ($this->versionManager->getNotes() as $note) { - $this->note($note); - } - } + $this->versionManager->updatePlugin($plugin); return $this; } @@ -713,7 +704,8 @@ class UpdateManager } $data = $this->requestServerData($type . '/popular'); - Cache::put($cacheKey, base64_encode(serialize($data)), 60); + $expiresAt = now()->addMinutes(60); + Cache::put($cacheKey, base64_encode(serialize($data)), $expiresAt); foreach ($data as $product) { $code = array_get($product, 'code', -1); @@ -802,35 +794,11 @@ class UpdateManager { if ($this->notesOutput !== null) { $this->notesOutput->writeln($message); - } else { - $this->notes[] = $message; } return $this; } - /** - * Get the notes for the last operation. - * @return array - */ - public function getNotes() - { - return $this->notes; - } - - /** - * Resets the notes store. - * @return self - */ - public function resetNotes() - { - $this->notesOutput = null; - - $this->notes = []; - - return $this; - } - /** * Sets an output stream for writing notes. * @param Illuminate\Console\Command $output diff --git a/modules/system/classes/VersionManager.php b/modules/system/classes/VersionManager.php index 77127cf12..11d425d9a 100644 --- a/modules/system/classes/VersionManager.php +++ b/modules/system/classes/VersionManager.php @@ -29,12 +29,6 @@ class VersionManager const HISTORY_TYPE_COMMENT = 'comment'; const HISTORY_TYPE_SCRIPT = 'script'; - /** - * The notes for the current operation. - * @var array - */ - protected $notes = []; - /** * @var \Illuminate\Console\OutputStyle */ @@ -426,6 +420,7 @@ class VersionManager * Execute the database PHP script */ $updateFile = $this->pluginManager->getPluginPath($code) . '/updates/' . $script; + $this->updater->packDown($updateFile); Db::table('system_plugin_history') @@ -508,35 +503,11 @@ class VersionManager { if ($this->notesOutput !== null) { $this->notesOutput->writeln($message); - } else { - $this->notes[] = $message; } return $this; } - /** - * Get the notes for the last operation. - * @return array - */ - public function getNotes() - { - return $this->notes; - } - - /** - * Resets the notes store. - * @return self - */ - public function resetNotes() - { - $this->notesOutput = null; - - $this->notes = []; - - return $this; - } - /** * Sets an output stream for writing notes. * @param Illuminate\Console\Command $output @@ -550,8 +521,7 @@ class VersionManager } /** - * @param $details - * + * Extract script and comments from version details * @return array */ protected function extractScriptsAndComments($details): array @@ -566,7 +536,8 @@ class VersionManager $scripts = array_values(array_filter($details, function ($detail) use ($fileNamePattern) { return preg_match($fileNamePattern, $detail); })); - } else { + } + else { $comments = (array)$details; $scripts = []; } diff --git a/modules/system/composer.json b/modules/system/composer.json index 5bebae7be..81d59a4dc 100644 --- a/modules/system/composer.json +++ b/modules/system/composer.json @@ -8,11 +8,13 @@ "authors": [ { "name": "Alexey Bobkov", - "email": "aleksey.bobkov@gmail.com" + "email": "aleksey.bobkov@gmail.com", + "role": "Co-founder" }, { "name": "Samuel Georges", - "email": "daftspunky@gmail.com" + "email": "daftspunky@gmail.com", + "role": "Co-founder" }, { "name": "Luke Towers", @@ -22,9 +24,10 @@ } ], "require": { - "php": ">=7.0", + "php": ">=7.2", "composer/installers": "~1.0", - "october/rain": "~1.0" + "october/rain": "~1.0", + "laravel/framework": "~6.0" }, "autoload": { "psr-4": { diff --git a/modules/system/console/OctoberEnv.php b/modules/system/console/OctoberEnv.php index cc735c2c4..43f2079bf 100644 --- a/modules/system/console/OctoberEnv.php +++ b/modules/system/console/OctoberEnv.php @@ -369,7 +369,7 @@ class OctoberEnv extends Command 'SESSION_DRIVER' => 'driver', ], 'queue' => [ - 'QUEUE_DRIVER' => 'default', + 'QUEUE_CONNECTION' => 'default', ], 'mail' => [ 'MAIL_DRIVER' => 'driver', diff --git a/modules/system/console/OctoberUpdate.php b/modules/system/console/OctoberUpdate.php index b826fe305..200dcd221 100644 --- a/modules/system/console/OctoberUpdate.php +++ b/modules/system/console/OctoberUpdate.php @@ -17,7 +17,6 @@ use Symfony\Component\Console\Input\InputOption; */ class OctoberUpdate extends Command { - /** * The console command name. */ diff --git a/modules/system/controllers/Settings.php b/modules/system/controllers/Settings.php index 7ba29dea7..719dff20e 100644 --- a/modules/system/controllers/Settings.php +++ b/modules/system/controllers/Settings.php @@ -2,6 +2,8 @@ use Lang; use Flash; +use Config; +use Request; use Backend; use BackendMenu; use System\Classes\SettingsManager; @@ -139,6 +141,22 @@ class Settings extends Controller return $this->formWidget->render($options); } + /** + * Returns the form widget used by this behavior. + * + * @return \Backend\Widgets\Form + */ + public function formGetWidget() + { + if (is_null($this->formWidget)) { + $item = $this->findSettingItem(); + $model = $this->createModel($item); + $this->initWidgets($model); + } + + return $this->formWidget; + } + /** * Prepare the widgets used by this action * Model $model @@ -169,10 +187,22 @@ class Settings extends Controller } /** - * Locates a setting item for a module or plugin + * Locates a setting item for a module or plugin. + * + * If none of the parameters are provided, they will be auto-guessed from the URL. + * + * @param string|null $author + * @param string|null $plugin + * @param string|null $code + * + * @return array */ - protected function findSettingItem($author, $plugin, $code) + protected function findSettingItem($author = null, $plugin = null, $code = null) { + if (is_null($author) || is_null($plugin)) { + [$author, $plugin, $code] = $this->guessSettingItem(); + } + $manager = SettingsManager::instance(); $moduleOwner = $author; @@ -187,4 +217,23 @@ class Settings extends Controller return $item; } + + /** + * Guesses the requested setting item from the current URL segments provided by the Request object. + * + * @return array + */ + protected function guessSettingItem() + { + $segments = Request::segments(); + + if (!empty(Config::get('cms.backendUri', 'backend'))) { + array_splice($segments, 0, 4); + } else { + array_splice($segments, 0, 3); + } + + // Ensure there's at least 3 segments + return array_pad($segments, 3, null); + } } diff --git a/modules/system/database/seeds/DatabaseSeeder.php b/modules/system/database/seeds/DatabaseSeeder.php index f2f924b13..7dec41339 100644 --- a/modules/system/database/seeds/DatabaseSeeder.php +++ b/modules/system/database/seeds/DatabaseSeeder.php @@ -13,8 +13,8 @@ class DatabaseSeeder extends Seeder */ public function run() { - Eloquent::unguard(); - - $this->call('System\Database\Seeds\SeedSetupMailLayouts'); + Eloquent::unguarded(function () { + $this->call('System\Database\Seeds\SeedSetupMailLayouts'); + }); } } diff --git a/modules/system/lang/en/validation.php b/modules/system/lang/en/validation.php index edc036dd0..ad6561c9e 100644 --- a/modules/system/lang/en/validation.php +++ b/modules/system/lang/en/validation.php @@ -32,6 +32,7 @@ return [ 'boolean' => 'The :attribute field must be true or false.', 'confirmed' => 'The :attribute confirmation does not match.', 'date' => 'The :attribute is not a valid date.', + 'date_equals' => 'The :attribute must be a date equal to :date.', 'date_format' => 'The :attribute does not match the format :format.', 'different' => 'The :attribute and :other must be different.', 'digits' => 'The :attribute must be :digits digits.', @@ -39,9 +40,22 @@ return [ 'dimensions' => 'The :attribute has invalid image dimensions.', 'distinct' => 'The :attribute field has a duplicate value.', 'email' => 'The :attribute must be a valid email address.', + 'ends_with' => 'The :attribute must end with one of the following: :values.', 'exists' => 'The selected :attribute is invalid.', 'file' => 'The :attribute must be a file.', 'filled' => 'The :attribute field must have a value.', + 'gt' => [ + 'numeric' => 'The :attribute must be greater than :value.', + 'file' => 'The :attribute must be greater than :value kilobytes.', + 'string' => 'The :attribute must be greater than :value characters.', + 'array' => 'The :attribute must have more than :value items.', + ], + 'gte' => [ + 'numeric' => 'The :attribute must be greater than or equal :value.', + 'file' => 'The :attribute must be greater than or equal :value kilobytes.', + 'string' => 'The :attribute must be greater than or equal :value characters.', + 'array' => 'The :attribute must have :value items or more.', + ], 'image' => 'The :attribute must be an image.', 'in' => 'The selected :attribute is invalid.', 'in_array' => 'The :attribute field does not exist in :other.', @@ -50,6 +64,18 @@ return [ 'ipv4' => 'The :attribute must be a valid IPv4 address.', 'ipv6' => 'The :attribute must be a valid IPv6 address.', 'json' => 'The :attribute must be a valid JSON string.', + 'lt' => [ + 'numeric' => 'The :attribute must be less than :value.', + 'file' => 'The :attribute must be less than :value kilobytes.', + 'string' => 'The :attribute must be less than :value characters.', + 'array' => 'The :attribute must have less than :value items.', + ], + 'lte' => [ + 'numeric' => 'The :attribute must be less than or equal :value.', + 'file' => 'The :attribute must be less than or equal :value kilobytes.', + 'string' => 'The :attribute must be less than or equal :value characters.', + 'array' => 'The :attribute must not have more than :value items.', + ], 'max' => [ 'numeric' => 'The :attribute may not be greater than :max.', 'file' => 'The :attribute may not be greater than :max kilobytes.', @@ -65,6 +91,7 @@ return [ 'array' => 'The :attribute must have at least :min items.', ], 'not_in' => 'The selected :attribute is invalid.', + 'not_regex' => 'The :attribute format is invalid.', 'numeric' => 'The :attribute must be a number.', 'present' => 'The :attribute field must be present.', 'regex' => 'The :attribute format is invalid.', @@ -82,11 +109,13 @@ return [ 'string' => 'The :attribute must be :size characters.', 'array' => 'The :attribute must contain :size items.', ], + 'starts_with' => 'The :attribute must start with one of the following: :values.', 'string' => 'The :attribute must be a string.', 'timezone' => 'The :attribute must be a valid zone.', 'unique' => 'The :attribute has already been taken.', 'uploaded' => 'The :attribute failed to upload.', 'url' => 'The :attribute format is invalid.', + 'uuid' => 'The :attribute must be a valid UUID.', /* |-------------------------------------------------------------------------- diff --git a/modules/system/models/mailsetting/fields.yaml b/modules/system/models/mailsetting/fields.yaml index c2457ec38..d49851cba 100644 --- a/modules/system/models/mailsetting/fields.yaml +++ b/modules/system/models/mailsetting/fields.yaml @@ -79,6 +79,7 @@ tabs: smtp_password: label: system::lang.mail.smtp_password tab: system::lang.mail.general + type: sensitive span: right trigger: action: show @@ -107,6 +108,7 @@ tabs: label: system::lang.mail.mailgun_secret commentAbove: system::lang.mail.mailgun_secret_comment tab: system::lang.mail.general + type: sensitive trigger: action: show field: send_mode @@ -116,6 +118,7 @@ tabs: label: system::lang.mail.mandrill_secret commentAbove: system::lang.mail.mandrill_secret_comment tab: system::lang.mail.general + type: sensitive trigger: action: show field: send_mode @@ -135,6 +138,7 @@ tabs: label: system::lang.mail.ses_secret commentAbove: system::lang.mail.ses_secret_comment tab: system::lang.mail.general + type: sensitive span: right trigger: action: show @@ -154,6 +158,7 @@ tabs: sparkpost_secret: label: system::lang.mail.sparkpost_secret commentAbove: system::lang.mail.sparkpost_secret_comment + type: sensitive tab: system::lang.mail.general trigger: action: show diff --git a/modules/system/providers.php b/modules/system/providers.php index 7b63edb43..96951f747 100644 --- a/modules/system/providers.php +++ b/modules/system/providers.php @@ -15,9 +15,7 @@ return [ Illuminate\Pagination\PaginationServiceProvider::class, Illuminate\Pipeline\PipelineServiceProvider::class, Illuminate\Queue\QueueServiceProvider::class, - Illuminate\Redis\RedisServiceProvider::class, Illuminate\Session\SessionServiceProvider::class, - Illuminate\Validation\ValidationServiceProvider::class, Illuminate\View\ViewServiceProvider::class, Laravel\Tinker\TinkerServiceProvider::class, @@ -36,5 +34,7 @@ return [ October\Rain\Flash\FlashServiceProvider::class, October\Rain\Mail\MailServiceProvider::class, October\Rain\Argon\ArgonServiceProvider::class, + October\Rain\Redis\RedisServiceProvider::class, + October\Rain\Validation\ValidationServiceProvider::class, ]; diff --git a/phpunit.xml b/phpunit.xml index 08cd19d58..de01655c0 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -8,7 +8,6 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - syntaxCheck="false" > diff --git a/tests/PluginTestCase.php b/tests/PluginTestCase.php index 6618b17e0..1d4a66f00 100644 --- a/tests/PluginTestCase.php +++ b/tests/PluginTestCase.php @@ -28,7 +28,7 @@ abstract class PluginTestCase extends TestCase $app['cache']->setDefaultDriver('array'); $app->setLocale('en'); - $app->singleton('auth', function ($app) { + $app->singleton('backend.auth', function ($app) { $app['auth.loaded'] = true; return AuthManager::instance(); @@ -67,7 +67,7 @@ abstract class PluginTestCase extends TestCase * Perform test case set up. * @return void */ - public function setUp() + public function setUp() : void { /* * Force reload of October singletons @@ -105,7 +105,7 @@ abstract class PluginTestCase extends TestCase * Flush event listeners and collect garbage. * @return void */ - public function tearDown() + public function tearDown() : void { $this->flushModelEventListeners(); parent::tearDown(); diff --git a/tests/README.md b/tests/README.md index a9e7b78b5..0e985d79b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,6 +1,6 @@ # Plugin testing -Plugin unit tests can be performed by running `phpunit` in the base plugin directory. +Individual plugin test cases can be run by running `../../../vendor/bin/phpunit` in the plugin's base directory (ex. `plugins/acme/demo`. ### Creating plugin tests @@ -58,7 +58,7 @@ The test class should extend the base class `PluginTestCase` and this is a speci class BaseTestCase extends PluginTestCase { - public function setUp() + public function setUp(): void { parent::setUp(); @@ -72,7 +72,7 @@ The test class should extend the base class `PluginTestCase` and this is a speci $pluginManager->bootAll(true); } - public function tearDown() + public function tearDown(): void { parent::tearDown(); @@ -96,39 +96,10 @@ To perform unit testing on the core October files, you should download a develop ### Unit tests -Unit tests can be performed by running `phpunit` in the root directory or inside `/tests/unit`. +Unit tests can be performed by running `vendor/bin/phpunit` in the root directory of your October CMS installation. ### Functional tests -Functional tests can be performed by running `phpunit` in the `/tests/functional` directory. Ensure the following configuration is met: +Functional tests can be performed by installing the [RainLab Dusk](https://octobercms.com/plugin/rainlab-dusk) in your October CMS installation. The RainLab Dusk plugin is powered by Laravel Dusk, a comprehensive testing suite for the Laravel framework that is designed to test interactions with a fully operational October CMS instance through a virtual browser. -- Active theme is `demo` -- Language preference is `en` - -#### Selenium set up - -1. Download latest Java SE from http://java.sun.com/ and install -1. Download a distribution archive of [Selenium Server](http://seleniumhq.org/download/). -1. Unzip the distribution archive and copy selenium-server-standalone-2.42.2.jar (check the version suffix) to /usr/local/bin, for instance. -1. Start the Selenium Server server by running `java -jar /usr/local/bin/selenium-server-standalone-2.42.2.jar`. - -#### Selenium configuration - -Create a new file `selenium.php` in the root directory, add the following content: - - markTestSkipped('Selenium skipped'); - } - - if (defined('TEST_SELENIUM_HOST')) { - $this->setHost(TEST_SELENIUM_HOST); - } - if (defined('TEST_SELENIUM_PORT')) { - $this->setPort(TEST_SELENIUM_PORT); - } - if (defined('TEST_SELENIUM_BROWSER')) { - $this->setBrowser(TEST_SELENIUM_BROWSER); - } - $this->setBrowserUrl(TEST_SELENIUM_URL); - } - - // - // OctoberCMS Helpers - // - - protected function signInToBackend() - { - $this->open('backend'); - $this->type("name=login", TEST_SELENIUM_USER); - $this->type("name=password", TEST_SELENIUM_PASS); - $this->click("//button[@type='submit']"); - $this->waitForPageToLoad("30000"); - } - - /** - * Similar to the native getConfirmation() function - */ - protected function getSweetConfirmation($expectedText = null, $clickOk = true) - { - $this->waitForElementPresent("xpath=(//div[@class='sweet-alert showSweetAlert visible'])[1]"); - - if ($expectedText) { - $this->verifyText("//div[@class='sweet-alert showSweetAlert visible']//h4", $expectedText); - } - - $this->verifyText("//div[@class='sweet-alert showSweetAlert visible']//button[@class='confirm btn btn-primary']", "OK"); - - if ($clickOk) { - $this->click("xpath=(//div[@class='sweet-alert showSweetAlert visible']//button[@class='confirm btn btn-primary'])[1]"); - } - } - - // - // Selenium helpers - // - - protected function waitForElementPresent($target, $timeout = 60) - { - $second = 0; - - while (true) { - if ($second >= $timeout) { - $this->fail('timeout'); - } - - try { - if ($this->isElementPresent($target)) { - break; - } - } - catch (Exception $e) { - } - - sleep(1); - ++$second; - } - } - - protected function waitForElementNotPresent($target, $timeout = 60) - { - $second = 0; - - while (true) { - if ($second >= $timeout) { - $this->fail('timeout'); - } - - try { - if (!$this->isElementPresent($target)) { - break; - } - } - catch (Exception $e) { - } - - sleep(1); - ++$second; - } - } -} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index e7e53fd1a..5ed3e1296 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -19,14 +19,3 @@ $loader->addDirectories([ 'modules', 'plugins' ]); - -/* - * Monkey patch PHPUnit\Framework\MockObject\Generator to avoid - * "Function ReflectionType::__toString() is deprecated" warnings - */ -$generatorPatchPath = __DIR__ . '/resources/patches/php-generator-7.php'; -$generatorSourcePath = __DIR__ . '/../vendor/phpunit/phpunit-mock-objects/src/Generator.php'; - -if (file_exists($generatorSourcePath)) { - file_put_contents($generatorSourcePath, file_get_contents($generatorPatchPath)); -} diff --git a/tests/concerns/InteractsWithAuthentication.php b/tests/concerns/InteractsWithAuthentication.php index a7950f230..5f5c3c8c7 100644 --- a/tests/concerns/InteractsWithAuthentication.php +++ b/tests/concerns/InteractsWithAuthentication.php @@ -29,7 +29,7 @@ trait InteractsWithAuthentication */ public function be(UserContract $user, $driver = null) { - $this->app['auth']->setUser($user); + $this->app['backend.auth']->setUser($user); } /** @@ -66,7 +66,7 @@ trait InteractsWithAuthentication */ protected function isAuthenticated($guard = null) { - return $this->app->make('auth')->guard($guard)->check(); + return $this->app->make('backend.auth')->guard($guard)->check(); } /** @@ -78,7 +78,7 @@ trait InteractsWithAuthentication */ public function assertAuthenticatedAs($user, $guard = null) { - $expected = $this->app->make('auth')->guard($guard)->user(); + $expected = $this->app->make('backend.auth')->guard($guard)->user(); $this->assertNotNull($expected, 'The current user is not authenticated.'); @@ -140,7 +140,7 @@ trait InteractsWithAuthentication */ protected function hasCredentials(array $credentials, $guard = null) { - $provider = $this->app->make('auth')->guard($guard)->getProvider(); + $provider = $this->app->make('backend.auth')->guard($guard)->getProvider(); $user = $provider->retrieveByCredentials($credentials); diff --git a/tests/fixtures/plugins/database/tester/models/Author.php b/tests/fixtures/plugins/database/tester/models/Author.php index 05e9a039f..2d9656fee 100644 --- a/tests/fixtures/plugins/database/tester/models/Author.php +++ b/tests/fixtures/plugins/database/tester/models/Author.php @@ -20,6 +20,7 @@ class Author extends Model */ public $belongsTo = [ 'user' => ['Database\Tester\Models\User', 'delete' => true], + 'country' => ['Database\Tester\Models\Country'], 'user_soft' => ['Database\Tester\Models\SoftDeleteUser', 'key' => 'user_id', 'softDelete' => true], ]; diff --git a/tests/fixtures/plugins/database/tester/models/Country.php b/tests/fixtures/plugins/database/tester/models/Country.php new file mode 100644 index 000000000..e7bb583c5 --- /dev/null +++ b/tests/fixtures/plugins/database/tester/models/Country.php @@ -0,0 +1,35 @@ + [ + 'Database\Tester\Models\User', + ], + ]; + + public $hasManyThrough = [ + 'posts' => [ + 'Database\Tester\Models\Post', + 'through' => 'Database\Tester\Models\Author', + ] + ]; +} + +class SoftDeleteCountry extends Country +{ + use \October\Rain\Database\Traits\SoftDelete; +} diff --git a/tests/fixtures/plugins/database/tester/models/User.php b/tests/fixtures/plugins/database/tester/models/User.php index 867992c2c..130120e9c 100644 --- a/tests/fixtures/plugins/database/tester/models/User.php +++ b/tests/fixtures/plugins/database/tester/models/User.php @@ -17,6 +17,19 @@ class User extends Model /** * @var array Relations */ + public $hasOne = [ + 'author' => [ + 'Database\Tester\Models\Author', + ] + ]; + + public $hasOneThrough = [ + 'phone' => [ + 'Database\Tester\Models\Phone', + 'through' => 'Database\Tester\Models\Author', + ], + ]; + public $attachOne = [ 'avatar' => 'System\Models\File' ]; diff --git a/tests/fixtures/plugins/database/tester/updates/create_authors_table.php b/tests/fixtures/plugins/database/tester/updates/create_authors_table.php index 6466e4e13..d6277e525 100644 --- a/tests/fixtures/plugins/database/tester/updates/create_authors_table.php +++ b/tests/fixtures/plugins/database/tester/updates/create_authors_table.php @@ -11,6 +11,7 @@ class CreateAuthorsTable extends Migration $table->engine = 'InnoDB'; $table->increments('id'); $table->integer('user_id')->unsigned()->index()->nullable(); + $table->integer('country_id')->unsigned()->index()->nullable(); $table->string('name')->nullable(); $table->string('email')->nullable(); $table->softDeletes(); diff --git a/tests/fixtures/plugins/database/tester/updates/create_countries_table.php b/tests/fixtures/plugins/database/tester/updates/create_countries_table.php new file mode 100644 index 000000000..7fc85ae54 --- /dev/null +++ b/tests/fixtures/plugins/database/tester/updates/create_countries_table.php @@ -0,0 +1,23 @@ +engine = 'InnoDB'; + $table->increments('id'); + $table->string('name')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('database_tester_countries'); + } +} diff --git a/tests/fixtures/plugins/database/tester/updates/version.yaml b/tests/fixtures/plugins/database/tester/updates/version.yaml index 613fc5337..590bb7892 100644 --- a/tests/fixtures/plugins/database/tester/updates/version.yaml +++ b/tests/fixtures/plugins/database/tester/updates/version.yaml @@ -9,3 +9,4 @@ - create_users_table.php - create_event_log_table.php - create_meta_table.php + - create_countries_table.php diff --git a/tests/fixtures/themes/test/assets/js/script1.js b/tests/fixtures/themes/test/assets/js/script1.js index e69de29bb..b8bbe4e13 100644 --- a/tests/fixtures/themes/test/assets/js/script1.js +++ b/tests/fixtures/themes/test/assets/js/script1.js @@ -0,0 +1 @@ +console.log('script1.js'); diff --git a/tests/fixtures/themes/test/assets/js/script2.js b/tests/fixtures/themes/test/assets/js/script2.js index e69de29bb..54e00dea3 100644 --- a/tests/fixtures/themes/test/assets/js/script2.js +++ b/tests/fixtures/themes/test/assets/js/script2.js @@ -0,0 +1 @@ +console.log('script2.js'); diff --git a/tests/fixtures/themes/test/assets/js/subdir/script1.js b/tests/fixtures/themes/test/assets/js/subdir/script1.js new file mode 100644 index 000000000..bc27e099c --- /dev/null +++ b/tests/fixtures/themes/test/assets/js/subdir/script1.js @@ -0,0 +1 @@ +console.log('subdir/script1.js'); diff --git a/tests/functional/backend/AuthTest.php b/tests/functional/backend/AuthTest.php deleted file mode 100644 index 4c1fe925d..000000000 --- a/tests/functional/backend/AuthTest.php +++ /dev/null @@ -1,90 +0,0 @@ -open('backend'); - - $cssLogoutLink = '#layout-mainmenu .mainmenu-accountmenu > ul > li:first-child > a'; - - try { - $this->assertTitle('Administration Area'); - $this->assertTrue($this->isElementPresent("name=login")); - $this->assertTrue($this->isElementPresent("name=password")); - $this->assertTrue($this->isElementPresent("//button[@type='submit']")); - $this->verifyText("//button[@type='submit']", "Login"); - } - catch (PHPUnit_Framework_AssertionFailedError $e) { - array_push($this->verificationErrors, $e->toString()); - } - - /* - * Sign in - */ - $this->type("name=login", TEST_SELENIUM_USER); - $this->type("name=password", TEST_SELENIUM_PASS); - $this->click("//button[@type='submit']"); - $this->waitForPageToLoad("30000"); - - try { - $this->assertTitle('Dashboard | October CMS'); - $this->assertTrue($this->isElementPresent('css='.$cssLogoutLink)); - } - catch (PHPUnit_Framework_AssertionFailedError $e) { - array_push($this->verificationErrors, $e->toString()); - } - - $this->verifyText('css='.$cssLogoutLink, "Sign out"); - - /* - * Log out - */ - $this->click('css='.$cssLogoutLink); - $this->waitForPageToLoad("30000"); - - try { - $this->assertTitle('Administration Area'); - } - catch (PHPUnit_Framework_AssertionFailedError $e) { - array_push($this->verificationErrors, $e->toString()); - } - } - - public function testPasswordReset() - { - $this->open('backend'); - - try { - $this->assertTrue($this->isElementPresent("link=exact:Forgot your password?")); - } - catch (PHPUnit_Framework_AssertionFailedError $e) { - array_push($this->verificationErrors, $e->toString()); - } - - $this->click('link=exact:Forgot your password?'); - $this->waitForPageToLoad("30000"); - - try { - $this->assertTrue($this->isElementPresent("//button[@type='submit']")); - $this->verifyText("//button[@type='submit']", "Restore"); - $this->assertTrue($this->isElementPresent("link=Cancel")); - } - catch (PHPUnit_Framework_AssertionFailedError $e) { - array_push($this->verificationErrors, $e->toString()); - } - - $this->type("name=login", TEST_SELENIUM_USER); - sleep(1); - $this->click("//button[@type='submit']"); - $this->waitForPageToLoad("30000"); - - try { - $this->assertTitle('Administration Area'); - $this->assertTrue($this->isElementPresent("css=p.flash-message.success")); - $this->verifyText("css=p.flash-message.success", "An email has been sent to your email address with password restore instructions.×"); - } - catch (PHPUnit_Framework_AssertionFailedError $e) { - array_push($this->verificationErrors, $e->toString()); - } - } -} diff --git a/tests/functional/cms/TemplateTest.php b/tests/functional/cms/TemplateTest.php deleted file mode 100644 index 5ff2a0a9e..000000000 --- a/tests/functional/cms/TemplateTest.php +++ /dev/null @@ -1,143 +0,0 @@ -signInToBackend(); - $this->open('cms'); - $this->waitForPageToLoad("30000"); - - // Fix the sidebar - $this->click("xpath=(//a[@class='fix-button'])[1]"); - - /* - * Page - */ - - // Create a new page - $this->click("xpath=(//form[@data-template-type='page']//button[@data-control='create-template'])[1]"); - $this->waitForElementPresent("name=settings[title]"); - - // Populate page details - $this->type('name=settings[title]', 'Functional Test Page'); - $this->type('name=settings[url]', '/xxx/functional/test/page'); - $this->type('name=fileName', 'xxx_functional_test_page'); - - // Save the new page - $this->click("xpath=(//a[@data-request='onSave'])[1]"); - $this->waitForElementPresent("xpath=(//li[@data-tab-id='page-".TEST_SELENIUM_THEME."-xxx_functional_test_page.htm'])[1]"); - - // Close the tab - $this->click("xpath=(//li[@data-tab-id='page-".TEST_SELENIUM_THEME."-xxx_functional_test_page.htm']/span[@class='tab-close'])[1]"); - - // Reopen the tab - $this->waitForElementPresent("xpath=(//div[@id='TemplateList-pageList-template-list']//li[@data-item-path='xxx_functional_test_page.htm']/a)[1]"); - $this->click("xpath=(//div[@id='TemplateList-pageList-template-list']//li[@data-item-path='xxx_functional_test_page.htm']/a)[1]"); - $this->waitForElementPresent("name=settings[title]"); - sleep(1); - - // Delete the page - $this->click("xpath=(//button[@data-request='onDelete'])[1]"); - $this->getSweetConfirmation('Do you really want delete this page?'); - // $this->assertTrue((bool)preg_match('/^Do you really want delete this page[\s\S]$/',$this->getConfirmation())); - $this->waitForElementNotPresent("name=settings[title]"); - - /* - * Partial - */ - - // Click partials menu item - $this->click("xpath=(//li[@data-menu-item='partials']/a)[1]"); - - // Create a new partial - $this->click("xpath=(//form[@data-template-type='partial']//button[@data-control='create-template'])[1]"); - $this->waitForElementPresent("name=fileName"); - - // Populate partial details - $this->type('name=fileName', 'xxx_functional_test_partial'); - $this->type('name=settings[description]', 'Test partial'); - - // Save the new partial - $this->click("xpath=(//a[@data-request='onSave'])[1]"); - $this->waitForElementPresent("xpath=(//li[@data-tab-id='partial-".TEST_SELENIUM_THEME."-xxx_functional_test_partial.htm'])[1]"); - - // Close the tab - $this->click("xpath=(//li[@data-tab-id='partial-".TEST_SELENIUM_THEME."-xxx_functional_test_partial.htm']/span[@class='tab-close'])[1]"); - - // Reopen the tab - $this->waitForElementPresent("xpath=(//div[@id='TemplateList-partialList-template-list']//li[@data-item-path='xxx_functional_test_partial.htm']/a)[1]"); - $this->click("xpath=(//div[@id='TemplateList-partialList-template-list']//li[@data-item-path='xxx_functional_test_partial.htm']/a)[1]"); - $this->waitForElementPresent("name=fileName"); - sleep(1); - - // Delete the partial - $this->click("xpath=(//button[@data-request='onDelete'])[1]"); - $this->getSweetConfirmation('Do you really want delete this partial?'); - $this->waitForElementNotPresent("name=fileName"); - - /* - * Layout - */ - - // Click layouts menu item - $this->click("xpath=(//li[@data-menu-item='layouts']/a)[1]"); - - // Create a new layout - $this->click("xpath=(//form[@data-template-type='layout']//button[@data-control='create-template'])[1]"); - $this->waitForElementPresent("name=fileName"); - - // Populate layout details - $this->type('name=fileName', 'xxx_functional_test_layout'); - $this->type('name=settings[description]', 'Test layout'); - - // Save the new layout - $this->click("xpath=(//a[@data-request='onSave'])[1]"); - $this->waitForElementPresent("xpath=(//li[@data-tab-id='layout-".TEST_SELENIUM_THEME."-xxx_functional_test_layout.htm'])[1]"); - - // Close the tab - $this->click("xpath=(//li[@data-tab-id='layout-".TEST_SELENIUM_THEME."-xxx_functional_test_layout.htm']/span[@class='tab-close'])[1]"); - - // Reopen the tab - $this->waitForElementPresent("xpath=(//div[@id='TemplateList-layoutList-template-list']//li[@data-item-path='xxx_functional_test_layout.htm']/a)[1]"); - $this->click("xpath=(//div[@id='TemplateList-layoutList-template-list']//li[@data-item-path='xxx_functional_test_layout.htm']/a)[1]"); - $this->waitForElementPresent("name=fileName"); - sleep(1); - - // Delete the layout - $this->click("xpath=(//button[@data-request='onDelete'])[1]"); - $this->getSweetConfirmation('Do you really want delete this layout?'); - $this->waitForElementNotPresent("name=fileName"); - - /* - * Content - */ - - // Click contents menu item - $this->click("xpath=(//li[@data-menu-item='content']/a)[1]"); - - // Create a new content - $this->click("xpath=(//form[@data-template-type='content']//button[@data-control='create-template'])[1]"); - $this->waitForElementPresent("name=fileName"); - - // Populate content details - $this->type('name=fileName', 'xxx_functional_test_content.txt'); - - // Save the new content - $this->click("xpath=(//a[@data-request='onSave'])[1]"); - $this->waitForElementPresent("xpath=(//li[@data-tab-id='content-".TEST_SELENIUM_THEME."-xxx_functional_test_content.txt'])[1]"); - - // Close the tab - $this->click("xpath=(//li[@data-tab-id='content-".TEST_SELENIUM_THEME."-xxx_functional_test_content.txt']/span[@class='tab-close'])[1]"); - - // Reopen the tab - $this->waitForElementPresent("xpath=(//div[@id='TemplateList-contentList-template-list']//li[@data-item-path='xxx_functional_test_content.txt']/a)[1]"); - $this->click("xpath=(//div[@id='TemplateList-contentList-template-list']//li[@data-item-path='xxx_functional_test_content.txt']/a)[1]"); - $this->waitForElementPresent("name=fileName"); - sleep(1); - - // Delete the content - $this->click("xpath=(//button[@data-request='onDelete'])[1]"); - $this->getSweetConfirmation('Do you really want delete this content file?'); - $this->waitForElementNotPresent("name=fileName"); - } -} diff --git a/tests/functional/phpunit.xml b/tests/functional/phpunit.xml deleted file mode 100644 index c8043149c..000000000 --- a/tests/functional/phpunit.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - ./ - - - \ No newline at end of file diff --git a/tests/resources/patches/php-generator-7.php b/tests/resources/patches/php-generator-7.php deleted file mode 100644 index 677680d69..000000000 --- a/tests/resources/patches/php-generator-7.php +++ /dev/null @@ -1,1185 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - * - * Patched with: https://github.com/sebastianbergmann/phpunit/pull/3765/files - */ -namespace PHPUnit\Framework\MockObject; - -use Doctrine\Instantiator\Exception\ExceptionInterface as InstantiatorException; -use Doctrine\Instantiator\Instantiator; -use Iterator; -use IteratorAggregate; -use PHPUnit\Framework\Exception; -use PHPUnit\Util\InvalidArgumentHelper; -use ReflectionClass; -use ReflectionException; -use ReflectionMethod; -use SoapClient; -use Text_Template; -use Traversable; - -/** - * Mock Object Code Generator - */ -class Generator -{ - /** - * @var array - */ - private static $cache = []; - - /** - * @var Text_Template[] - */ - private static $templates = []; - - /** - * @var array - */ - private $blacklistedMethodNames = [ - '__CLASS__' => true, - '__DIR__' => true, - '__FILE__' => true, - '__FUNCTION__' => true, - '__LINE__' => true, - '__METHOD__' => true, - '__NAMESPACE__' => true, - '__TRAIT__' => true, - '__clone' => true, - '__halt_compiler' => true, - ]; - - /** - * Returns a mock object for the specified class. - * - * @param string|string[] $type - * @param array $methods - * @param array $arguments - * @param string $mockClassName - * @param bool $callOriginalConstructor - * @param bool $callOriginalClone - * @param bool $callAutoload - * @param bool $cloneArguments - * @param bool $callOriginalMethods - * @param object $proxyTarget - * @param bool $allowMockingUnknownTypes - * - * @return MockObject - * - * @throws Exception - * @throws RuntimeException - * @throws \PHPUnit\Framework\Exception - * @throws \ReflectionException - */ - public function getMock($type, $methods = [], array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $cloneArguments = true, $callOriginalMethods = false, $proxyTarget = null, $allowMockingUnknownTypes = true) - { - if (!\is_array($type) && !\is_string($type)) { - throw InvalidArgumentHelper::factory(1, 'array or string'); - } - - if (!\is_string($mockClassName)) { - throw InvalidArgumentHelper::factory(4, 'string'); - } - - if (!\is_array($methods) && null !== $methods) { - throw InvalidArgumentHelper::factory(2, 'array', $methods); - } - - if ($type === 'Traversable' || $type === '\\Traversable') { - $type = 'Iterator'; - } - - if (\is_array($type)) { - $type = \array_unique( - \array_map( - function ($type) { - if ($type === 'Traversable' || - $type === '\\Traversable' || - $type === '\\Iterator') { - return 'Iterator'; - } - - return $type; - }, - $type - ) - ); - } - - if (!$allowMockingUnknownTypes) { - if (\is_array($type)) { - foreach ($type as $_type) { - if (!\class_exists($_type, $callAutoload) && - !\interface_exists($_type, $callAutoload)) { - throw new RuntimeException( - \sprintf( - 'Cannot stub or mock class or interface "%s" which does not exist', - $_type - ) - ); - } - } - } else { - if (!\class_exists($type, $callAutoload) && - !\interface_exists($type, $callAutoload) - ) { - throw new RuntimeException( - \sprintf( - 'Cannot stub or mock class or interface "%s" which does not exist', - $type - ) - ); - } - } - } - - if (null !== $methods) { - foreach ($methods as $method) { - if (!\preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*~', $method)) { - throw new RuntimeException( - \sprintf( - 'Cannot stub or mock method with invalid name "%s"', - $method - ) - ); - } - } - - if ($methods !== \array_unique($methods)) { - throw new RuntimeException( - \sprintf( - 'Cannot stub or mock using a method list that contains duplicates: "%s" (duplicate: "%s")', - \implode(', ', $methods), - \implode(', ', \array_unique(\array_diff_assoc($methods, \array_unique($methods)))) - ) - ); - } - } - - if ($mockClassName !== '' && \class_exists($mockClassName, false)) { - $reflect = new ReflectionClass($mockClassName); - - if (!$reflect->implementsInterface(MockObject::class)) { - throw new RuntimeException( - \sprintf( - 'Class "%s" already exists.', - $mockClassName - ) - ); - } - } - - if ($callOriginalConstructor === false && $callOriginalMethods === true) { - throw new RuntimeException( - 'Proxying to original methods requires invoking the original constructor' - ); - } - - $mock = $this->generate( - $type, - $methods, - $mockClassName, - $callOriginalClone, - $callAutoload, - $cloneArguments, - $callOriginalMethods - ); - - return $this->getObject( - $mock['code'], - $mock['mockClassName'], - $type, - $callOriginalConstructor, - $callAutoload, - $arguments, - $callOriginalMethods, - $proxyTarget - ); - } - - /** - * @param string $code - * @param string $className - * @param array|string $type - * @param bool $callOriginalConstructor - * @param bool $callAutoload - * @param array $arguments - * @param bool $callOriginalMethods - * @param object $proxyTarget - * - * @return MockObject - * - * @throws \ReflectionException - * @throws RuntimeException - */ - private function getObject($code, $className, $type = '', $callOriginalConstructor = false, $callAutoload = false, array $arguments = [], $callOriginalMethods = false, $proxyTarget = null) - { - $this->evalClass($code, $className); - - if ($callOriginalConstructor && - \is_string($type) && - !\interface_exists($type, $callAutoload)) { - if (\count($arguments) === 0) { - $object = new $className; - } else { - $class = new ReflectionClass($className); - $object = $class->newInstanceArgs($arguments); - } - } else { - try { - $instantiator = new Instantiator; - $object = $instantiator->instantiate($className); - } catch (InstantiatorException $exception) { - throw new RuntimeException($exception->getMessage()); - } - } - - if ($callOriginalMethods) { - if (!\is_object($proxyTarget)) { - if (\count($arguments) === 0) { - $proxyTarget = new $type; - } else { - $class = new ReflectionClass($type); - $proxyTarget = $class->newInstanceArgs($arguments); - } - } - - $object->__phpunit_setOriginalObject($proxyTarget); - } - - return $object; - } - - /** - * @param string $code - * @param string $className - */ - private function evalClass($code, $className) - { - if (!\class_exists($className, false)) { - eval($code); - } - } - - /** - * Returns a mock object for the specified abstract class with all abstract - * methods of the class mocked. Concrete methods to mock can be specified with - * the last parameter - * - * @param string $originalClassName - * @param array $arguments - * @param string $mockClassName - * @param bool $callOriginalConstructor - * @param bool $callOriginalClone - * @param bool $callAutoload - * @param array $mockedMethods - * @param bool $cloneArguments - * - * @return MockObject - * - * @throws \ReflectionException - * @throws RuntimeException - * @throws Exception - */ - public function getMockForAbstractClass($originalClassName, array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = [], $cloneArguments = true) - { - if (!\is_string($originalClassName)) { - throw InvalidArgumentHelper::factory(1, 'string'); - } - - if (!\is_string($mockClassName)) { - throw InvalidArgumentHelper::factory(3, 'string'); - } - - if (\class_exists($originalClassName, $callAutoload) || - \interface_exists($originalClassName, $callAutoload)) { - $reflector = new ReflectionClass($originalClassName); - $methods = $mockedMethods; - - foreach ($reflector->getMethods() as $method) { - if ($method->isAbstract() && !\in_array($method->getName(), $methods)) { - $methods[] = $method->getName(); - } - } - - if (empty($methods)) { - $methods = null; - } - - return $this->getMock( - $originalClassName, - $methods, - $arguments, - $mockClassName, - $callOriginalConstructor, - $callOriginalClone, - $callAutoload, - $cloneArguments - ); - } - - throw new RuntimeException( - \sprintf('Class "%s" does not exist.', $originalClassName) - ); - } - - /** - * Returns a mock object for the specified trait with all abstract methods - * of the trait mocked. Concrete methods to mock can be specified with the - * `$mockedMethods` parameter. - * - * @param string $traitName - * @param array $arguments - * @param string $mockClassName - * @param bool $callOriginalConstructor - * @param bool $callOriginalClone - * @param bool $callAutoload - * @param array $mockedMethods - * @param bool $cloneArguments - * - * @return MockObject - * - * @throws \ReflectionException - * @throws RuntimeException - * @throws Exception - */ - public function getMockForTrait($traitName, array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = [], $cloneArguments = true) - { - if (!\is_string($traitName)) { - throw InvalidArgumentHelper::factory(1, 'string'); - } - - if (!\is_string($mockClassName)) { - throw InvalidArgumentHelper::factory(3, 'string'); - } - - if (!\trait_exists($traitName, $callAutoload)) { - throw new RuntimeException( - \sprintf( - 'Trait "%s" does not exist.', - $traitName - ) - ); - } - - $className = $this->generateClassName( - $traitName, - '', - 'Trait_' - ); - - $classTemplate = $this->getTemplate('trait_class.tpl'); - - $classTemplate->setVar( - [ - 'prologue' => 'abstract ', - 'class_name' => $className['className'], - 'trait_name' => $traitName - ] - ); - - $this->evalClass( - $classTemplate->render(), - $className['className'] - ); - - return $this->getMockForAbstractClass($className['className'], $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $mockedMethods, $cloneArguments); - } - - /** - * Returns an object for the specified trait. - * - * @param string $traitName - * @param array $arguments - * @param string $traitClassName - * @param bool $callOriginalConstructor - * @param bool $callOriginalClone - * @param bool $callAutoload - * - * @return object - * - * @throws \ReflectionException - * @throws RuntimeException - * @throws Exception - */ - public function getObjectForTrait($traitName, array $arguments = [], $traitClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true) - { - if (!\is_string($traitName)) { - throw InvalidArgumentHelper::factory(1, 'string'); - } - - if (!\is_string($traitClassName)) { - throw InvalidArgumentHelper::factory(3, 'string'); - } - - if (!\trait_exists($traitName, $callAutoload)) { - throw new RuntimeException( - \sprintf( - 'Trait "%s" does not exist.', - $traitName - ) - ); - } - - $className = $this->generateClassName( - $traitName, - $traitClassName, - 'Trait_' - ); - - $classTemplate = $this->getTemplate('trait_class.tpl'); - - $classTemplate->setVar( - [ - 'prologue' => '', - 'class_name' => $className['className'], - 'trait_name' => $traitName - ] - ); - - return $this->getObject($classTemplate->render(), $className['className']); - } - - /** - * @param array|string $type - * @param array $methods - * @param string $mockClassName - * @param bool $callOriginalClone - * @param bool $callAutoload - * @param bool $cloneArguments - * @param bool $callOriginalMethods - * - * @return array - * - * @throws \ReflectionException - * @throws \PHPUnit\Framework\MockObject\RuntimeException - */ - public function generate($type, array $methods = null, $mockClassName = '', $callOriginalClone = true, $callAutoload = true, $cloneArguments = true, $callOriginalMethods = false) - { - if (\is_array($type)) { - \sort($type); - } - - if ($mockClassName === '') { - $key = \md5( - \is_array($type) ? \implode('_', $type) : $type . - \serialize($methods) . - \serialize($callOriginalClone) . - \serialize($cloneArguments) . - \serialize($callOriginalMethods) - ); - - if (isset(self::$cache[$key])) { - return self::$cache[$key]; - } - } - - $mock = $this->generateMock( - $type, - $methods, - $mockClassName, - $callOriginalClone, - $callAutoload, - $cloneArguments, - $callOriginalMethods - ); - - if (isset($key)) { - self::$cache[$key] = $mock; - } - - return $mock; - } - - /** - * @param string $wsdlFile - * @param string $className - * @param array $methods - * @param array $options - * - * @return string - * - * @throws RuntimeException - */ - public function generateClassFromWsdl($wsdlFile, $className, array $methods = [], array $options = []) - { - if (!\extension_loaded('soap')) { - throw new RuntimeException( - 'The SOAP extension is required to generate a mock object from WSDL.' - ); - } - - $options = \array_merge($options, ['cache_wsdl' => WSDL_CACHE_NONE]); - $client = new SoapClient($wsdlFile, $options); - $_methods = \array_unique($client->__getFunctions()); - unset($client); - - \sort($_methods); - - $methodTemplate = $this->getTemplate('wsdl_method.tpl'); - $methodsBuffer = ''; - - foreach ($_methods as $method) { - $nameStart = \strpos($method, ' ') + 1; - $nameEnd = \strpos($method, '('); - $name = \substr($method, $nameStart, $nameEnd - $nameStart); - - if (empty($methods) || \in_array($name, $methods)) { - $args = \explode( - ',', - \substr( - $method, - $nameEnd + 1, - \strpos($method, ')') - $nameEnd - 1 - ) - ); - - foreach (\range(0, \count($args) - 1) as $i) { - $args[$i] = \substr($args[$i], \strpos($args[$i], '$')); - } - - $methodTemplate->setVar( - [ - 'method_name' => $name, - 'arguments' => \implode(', ', $args) - ] - ); - - $methodsBuffer .= $methodTemplate->render(); - } - } - - $optionsBuffer = 'array('; - - foreach ($options as $key => $value) { - $optionsBuffer .= $key . ' => ' . $value; - } - - $optionsBuffer .= ')'; - - $classTemplate = $this->getTemplate('wsdl_class.tpl'); - $namespace = ''; - - if (\strpos($className, '\\') !== false) { - $parts = \explode('\\', $className); - $className = \array_pop($parts); - $namespace = 'namespace ' . \implode('\\', $parts) . ';' . "\n\n"; - } - - $classTemplate->setVar( - [ - 'namespace' => $namespace, - 'class_name' => $className, - 'wsdl' => $wsdlFile, - 'options' => $optionsBuffer, - 'methods' => $methodsBuffer - ] - ); - - return $classTemplate->render(); - } - - /** - * @param array|string $type - * @param array|null $methods - * @param string $mockClassName - * @param bool $callOriginalClone - * @param bool $callAutoload - * @param bool $cloneArguments - * @param bool $callOriginalMethods - * - * @return array - * - * @throws \InvalidArgumentException - * @throws \ReflectionException - * @throws RuntimeException - */ - private function generateMock($type, $methods, $mockClassName, $callOriginalClone, $callAutoload, $cloneArguments, $callOriginalMethods) - { - $methodReflections = []; - $classTemplate = $this->getTemplate('mocked_class.tpl'); - - $additionalInterfaces = []; - $cloneTemplate = ''; - $isClass = false; - $isInterface = false; - $isMultipleInterfaces = false; - - if (\is_array($type)) { - foreach ($type as $_type) { - if (!\interface_exists($_type, $callAutoload)) { - throw new RuntimeException( - \sprintf( - 'Interface "%s" does not exist.', - $_type - ) - ); - } - - $isMultipleInterfaces = true; - - $additionalInterfaces[] = $_type; - $typeClass = new ReflectionClass($this->generateClassName( - $_type, - $mockClassName, - 'Mock_' - )['fullClassName'] - ); - - foreach ($this->getClassMethods($_type) as $method) { - if (\in_array($method, $methods)) { - throw new RuntimeException( - \sprintf( - 'Duplicate method "%s" not allowed.', - $method - ) - ); - } - - $methodReflections[$method] = $typeClass->getMethod($method); - $methods[] = $method; - } - } - } - - $mockClassName = $this->generateClassName( - $type, - $mockClassName, - 'Mock_' - ); - - if (\class_exists($mockClassName['fullClassName'], $callAutoload)) { - $isClass = true; - } elseif (\interface_exists($mockClassName['fullClassName'], $callAutoload)) { - $isInterface = true; - } - - if (!$isClass && !$isInterface) { - $prologue = 'class ' . $mockClassName['originalClassName'] . "\n{\n}\n\n"; - - if (!empty($mockClassName['namespaceName'])) { - $prologue = 'namespace ' . $mockClassName['namespaceName'] . - " {\n\n" . $prologue . "}\n\n" . - "namespace {\n\n"; - - $epilogue = "\n\n}"; - } - - $cloneTemplate = $this->getTemplate('mocked_clone.tpl'); - } else { - $class = new ReflectionClass($mockClassName['fullClassName']); - - if ($class->isFinal()) { - throw new RuntimeException( - \sprintf( - 'Class "%s" is declared "final" and cannot be mocked.', - $mockClassName['fullClassName'] - ) - ); - } - - if ($class->hasMethod('__clone')) { - $cloneMethod = $class->getMethod('__clone'); - - if (!$cloneMethod->isFinal()) { - if ($callOriginalClone && !$isInterface) { - $cloneTemplate = $this->getTemplate('unmocked_clone.tpl'); - } else { - $cloneTemplate = $this->getTemplate('mocked_clone.tpl'); - } - } - } else { - $cloneTemplate = $this->getTemplate('mocked_clone.tpl'); - } - } - - if (\is_object($cloneTemplate)) { - $cloneTemplate = $cloneTemplate->render(); - } - - if (\is_array($methods) && empty($methods) && - ($isClass || $isInterface)) { - $methods = $this->getClassMethods($mockClassName['fullClassName']); - } - - if (!\is_array($methods)) { - $methods = []; - } - - $mockedMethods = ''; - $configurable = []; - - foreach ($methods as $methodName) { - if ($methodName !== '__construct' && $methodName !== '__clone') { - $configurable[] = \strtolower($methodName); - } - } - - if (isset($class)) { - // https://github.com/sebastianbergmann/phpunit-mock-objects/issues/103 - if ($isInterface && $class->implementsInterface(Traversable::class) && - !$class->implementsInterface(Iterator::class) && - !$class->implementsInterface(IteratorAggregate::class)) { - $additionalInterfaces[] = Iterator::class; - $methods = \array_merge($methods, $this->getClassMethods(Iterator::class)); - } - - foreach ($methods as $methodName) { - try { - $method = $class->getMethod($methodName); - - if ($this->canMockMethod($method)) { - $mockedMethods .= $this->generateMockedMethodDefinitionFromExisting( - $method, - $cloneArguments, - $callOriginalMethods - ); - } - } catch (ReflectionException $e) { - $mockedMethods .= $this->generateMockedMethodDefinition( - $mockClassName['fullClassName'], - $methodName, - $cloneArguments - ); - } - } - } elseif ($isMultipleInterfaces) { - foreach ($methods as $methodName) { - if ($this->canMockMethod($methodReflections[$methodName])) { - $mockedMethods .= $this->generateMockedMethodDefinitionFromExisting( - $methodReflections[$methodName], - $cloneArguments, - $callOriginalMethods - ); - } - } - } else { - foreach ($methods as $methodName) { - $mockedMethods .= $this->generateMockedMethodDefinition( - $mockClassName['fullClassName'], - $methodName, - $cloneArguments - ); - } - } - - $method = ''; - - if (!\in_array('method', $methods) && (!isset($class) || !$class->hasMethod('method'))) { - $methodTemplate = $this->getTemplate('mocked_class_method.tpl'); - - $method = $methodTemplate->render(); - } - - $classTemplate->setVar( - [ - 'prologue' => $prologue ?? '', - 'epilogue' => $epilogue ?? '', - 'class_declaration' => $this->generateMockClassDeclaration( - $mockClassName, - $isInterface, - $additionalInterfaces - ), - 'clone' => $cloneTemplate, - 'mock_class_name' => $mockClassName['className'], - 'mocked_methods' => $mockedMethods, - 'method' => $method, - 'configurable' => '[' . \implode(', ', \array_map(function ($m) { - return '\'' . $m . '\''; - }, $configurable)) . ']' - ] - ); - - return [ - 'code' => $classTemplate->render(), - 'mockClassName' => $mockClassName['className'] - ]; - } - - /** - * @param array|string $type - * @param string $className - * @param string $prefix - * - * @return array - */ - private function generateClassName($type, $className, $prefix) - { - if (\is_array($type)) { - $type = \implode('_', $type); - } - - if ($type[0] === '\\') { - $type = \substr($type, 1); - } - - $classNameParts = \explode('\\', $type); - - if (\count($classNameParts) > 1) { - $type = \array_pop($classNameParts); - $namespaceName = \implode('\\', $classNameParts); - $fullClassName = $namespaceName . '\\' . $type; - } else { - $namespaceName = ''; - $fullClassName = $type; - } - - if ($className === '') { - do { - $className = $prefix . $type . '_' . - \substr(\md5(\mt_rand()), 0, 8); - } while (\class_exists($className, false)); - } - - return [ - 'className' => $className, - 'originalClassName' => $type, - 'fullClassName' => $fullClassName, - 'namespaceName' => $namespaceName - ]; - } - - /** - * @param array $mockClassName - * @param bool $isInterface - * @param array $additionalInterfaces - * - * @return string - */ - private function generateMockClassDeclaration(array $mockClassName, $isInterface, array $additionalInterfaces = []) - { - $buffer = 'class '; - - $additionalInterfaces[] = MockObject::class; - $interfaces = \implode(', ', $additionalInterfaces); - - if ($isInterface) { - $buffer .= \sprintf( - '%s implements %s', - $mockClassName['className'], - $interfaces - ); - - if (!\in_array($mockClassName['originalClassName'], $additionalInterfaces)) { - $buffer .= ', '; - - if (!empty($mockClassName['namespaceName'])) { - $buffer .= $mockClassName['namespaceName'] . '\\'; - } - - $buffer .= $mockClassName['originalClassName']; - } - } else { - $buffer .= \sprintf( - '%s extends %s%s implements %s', - $mockClassName['className'], - !empty($mockClassName['namespaceName']) ? $mockClassName['namespaceName'] . '\\' : '', - $mockClassName['originalClassName'], - $interfaces - ); - } - - return $buffer; - } - - /** - * @param ReflectionMethod $method - * @param bool $cloneArguments - * @param bool $callOriginalMethods - * - * @return string - * - * @throws \PHPUnit\Framework\MockObject\RuntimeException - */ - private function generateMockedMethodDefinitionFromExisting(ReflectionMethod $method, $cloneArguments, $callOriginalMethods) - { - if ($method->isPrivate()) { - $modifier = 'private'; - } elseif ($method->isProtected()) { - $modifier = 'protected'; - } else { - $modifier = 'public'; - } - - if ($method->isStatic()) { - $modifier .= ' static'; - } - - if ($method->returnsReference()) { - $reference = '&'; - } else { - $reference = ''; - } - - if ($method->hasReturnType()) { - $returnType = $method->getReturnType()->getName(); - } else { - $returnType = ''; - } - - if (\preg_match('#\*[ \t]*+@deprecated[ \t]*+(.*?)\r?+\n[ \t]*+\*(?:[ \t]*+@|/$)#s', $method->getDocComment(), $deprecation)) { - $deprecation = \trim(\preg_replace('#[ \t]*\r?\n[ \t]*+\*[ \t]*+#', ' ', $deprecation[1])); - } else { - $deprecation = false; - } - - return $this->generateMockedMethodDefinition( - $method->getDeclaringClass()->getName(), - $method->getName(), - $cloneArguments, - $modifier, - $this->getMethodParameters($method), - $this->getMethodParameters($method, true), - $returnType, - $reference, - $callOriginalMethods, - $method->isStatic(), - $deprecation, - $method->hasReturnType() && PHP_VERSION_ID >= 70100 && $method->getReturnType()->allowsNull() - ); - } - - /** - * @param string $className - * @param string $methodName - * @param bool $cloneArguments - * @param string $modifier - * @param string $argumentsForDeclaration - * @param string $argumentsForCall - * @param string $returnType - * @param string $reference - * @param bool $callOriginalMethods - * @param bool $static - * @param bool|string $deprecation - * @param bool $allowsReturnNull - * - * @return string - * - * @throws \InvalidArgumentException - */ - private function generateMockedMethodDefinition($className, $methodName, $cloneArguments = true, $modifier = 'public', $argumentsForDeclaration = '', $argumentsForCall = '', $returnType = '', $reference = '', $callOriginalMethods = false, $static = false, $deprecation = false, $allowsReturnNull = false) - { - if ($static) { - $templateFile = 'mocked_static_method.tpl'; - } else { - if ($returnType === 'void') { - $templateFile = \sprintf( - '%s_method_void.tpl', - $callOriginalMethods ? 'proxied' : 'mocked' - ); - } else { - $templateFile = \sprintf( - '%s_method.tpl', - $callOriginalMethods ? 'proxied' : 'mocked' - ); - } - } - - // Mocked interfaces returning 'self' must explicitly declare the - // interface name as the return type. See - // https://bugs.php.net/bug.php?id=70722 - if ($returnType === 'self') { - $returnType = $className; - } - - if (false !== $deprecation) { - $deprecation = "The $className::$methodName method is deprecated ($deprecation)."; - $deprecationTemplate = $this->getTemplate('deprecation.tpl'); - - $deprecationTemplate->setVar( - [ - 'deprecation' => \var_export($deprecation, true), - ] - ); - - $deprecation = $deprecationTemplate->render(); - } - - $template = $this->getTemplate($templateFile); - - $template->setVar( - [ - 'arguments_decl' => $argumentsForDeclaration, - 'arguments_call' => $argumentsForCall, - 'return_delim' => $returnType ? ': ' : '', - 'return_type' => $allowsReturnNull ? '?' . $returnType : $returnType, - 'arguments_count' => !empty($argumentsForCall) ? \substr_count($argumentsForCall, ',') + 1 : 0, - 'class_name' => $className, - 'method_name' => $methodName, - 'modifier' => $modifier, - 'reference' => $reference, - 'clone_arguments' => $cloneArguments ? 'true' : 'false', - 'deprecation' => $deprecation - ] - ); - - return $template->render(); - } - - /** - * @param ReflectionMethod $method - * - * @return bool - * - * @throws \ReflectionException - */ - private function canMockMethod(ReflectionMethod $method) - { - return !($method->isConstructor() || $method->isFinal() || $method->isPrivate() || $this->isMethodNameBlacklisted($method->getName())); - } - - /** - * Returns whether a method name is blacklisted - * - * @param string $name - * - * @return bool - */ - private function isMethodNameBlacklisted($name) - { - return isset($this->blacklistedMethodNames[$name]); - } - - /** - * Returns the parameters of a function or method. - * - * @param ReflectionMethod $method - * @param bool $forCall - * - * @return string - * - * @throws RuntimeException - */ - private function getMethodParameters(ReflectionMethod $method, $forCall = false) - { - $parameters = []; - - foreach ($method->getParameters() as $i => $parameter) { - $name = '$' . $parameter->getName(); - - /* Note: PHP extensions may use empty names for reference arguments - * or "..." for methods taking a variable number of arguments. - */ - if ($name === '$' || $name === '$...') { - $name = '$arg' . $i; - } - - if ($parameter->isVariadic()) { - if ($forCall) { - continue; - } - - $name = '...' . $name; - } - - $nullable = ''; - $default = ''; - $reference = ''; - $typeDeclaration = ''; - - if (!$forCall) { - if (PHP_VERSION_ID >= 70100 && $parameter->hasType() && $parameter->allowsNull()) { - $nullable = '?'; - } - - if ($parameter->hasType() && $parameter->getType()->getName() !== 'self') { - $typeDeclaration = $parameter->getType()->getName() . ' '; - } elseif ($parameter->isArray()) { - $typeDeclaration = 'array '; - } elseif ($parameter->isCallable()) { - $typeDeclaration = 'callable '; - } else { - try { - $class = $parameter->getClass(); - } catch (ReflectionException $e) { - throw new RuntimeException( - \sprintf( - 'Cannot mock %s::%s() because a class or ' . - 'interface used in the signature is not loaded', - $method->getDeclaringClass()->getName(), - $method->getName() - ), - 0, - $e - ); - } - - if ($class !== null) { - $typeDeclaration = $class->getName() . ' '; - } - } - - if (!$parameter->isVariadic()) { - if ($parameter->isDefaultValueAvailable()) { - $value = $parameter->getDefaultValueConstantName(); - - if ($value === null) { - $value = \var_export($parameter->getDefaultValue(), true); - } elseif (!\defined($value)) { - $rootValue = \preg_replace('/^.*\\\\/', '', $value); - $value = \defined($rootValue) ? $rootValue : $value; - } - - $default = ' = ' . $value; - } elseif ($parameter->isOptional()) { - $default = ' = null'; - } - } - } - - if ($parameter->isPassedByReference()) { - $reference = '&'; - } - - $parameters[] = $nullable . $typeDeclaration . $reference . $name . $default; - } - - return \implode(', ', $parameters); - } - - /** - * @param string $className - * - * @return array - * - * @throws \ReflectionException - */ - public function getClassMethods($className) - { - $class = new ReflectionClass($className); - $methods = []; - - foreach ($class->getMethods() as $method) { - if ($method->isPublic() || $method->isAbstract()) { - $methods[] = $method->getName(); - } - } - - return $methods; - } - - /** - * @param string $template - * - * @return Text_Template - * - * @throws \InvalidArgumentException - */ - private function getTemplate($template) - { - $filename = __DIR__ . DIRECTORY_SEPARATOR . 'Generator' . DIRECTORY_SEPARATOR . $template; - - if (!isset(self::$templates[$filename])) { - self::$templates[$filename] = new Text_Template($filename); - } - - return self::$templates[$filename]; - } -} diff --git a/tests/unit/backend/classes/AuthManagerTest.php b/tests/unit/backend/classes/AuthManagerTest.php index 6bb9f9684..65dd5e735 100644 --- a/tests/unit/backend/classes/AuthManagerTest.php +++ b/tests/unit/backend/classes/AuthManagerTest.php @@ -4,7 +4,7 @@ use October\Rain\Exception\SystemException; class AuthManagerTest extends TestCase { - public function setUp() + public function setUp(): void { $this->createApplication(); @@ -23,7 +23,7 @@ class AuthManagerTest extends TestCase ]); } - public function tearDown() + public function tearDown(): void { AuthManager::forgetInstance(); } diff --git a/tests/unit/backend/classes/NavigationManagerTest.php b/tests/unit/backend/classes/NavigationManagerTest.php index 04ca2f183..f63645198 100644 --- a/tests/unit/backend/classes/NavigationManagerTest.php +++ b/tests/unit/backend/classes/NavigationManagerTest.php @@ -59,18 +59,18 @@ class NavigationManagerTest extends TestCase $manager->setContext('October.Tester', 'blog'); $items = $manager->listSideMenuItems(); - $this->assertInternalType('array', $items); + $this->assertIsArray($items); $this->assertArrayHasKey('posts', $items); $this->assertArrayHasKey('categories', $items); - $this->assertInternalType('object', $items['posts']); + $this->assertIsObject($items['posts']); $this->assertObjectHasAttribute('code', $items['posts']); $this->assertObjectHasAttribute('owner', $items['posts']); $this->assertEquals('posts', $items['posts']->code); $this->assertEquals('October.Tester', $items['posts']->owner); $this->assertObjectHasAttribute('permissions', $items['posts']); - $this->assertInternalType('array', $items['posts']->permissions); + $this->assertIsArray($items['posts']->permissions); $this->assertCount(1, $items['posts']->permissions); $this->assertObjectHasAttribute('order', $items['posts']); @@ -92,7 +92,7 @@ class NavigationManagerTest extends TestCase $items = $manager->listMainMenuItems(); - $this->assertInternalType('array', $items); + $this->assertIsArray($items); $this->assertArrayHasKey('OCTOBER.TESTER.PRINT', $items); $item = $items['OCTOBER.TESTER.PRINT']; @@ -143,10 +143,10 @@ class NavigationManagerTest extends TestCase $manager->setContext('October.Tester', 'blog'); $items = $manager->listSideMenuItems(); - $this->assertInternalType('array', $items); + $this->assertIsArray($items); $this->assertArrayHasKey('foo', $items); - $this->assertInternalType('object', $items['foo']); + $this->assertIsObject($items['foo']); $this->assertObjectHasAttribute('code', $items['foo']); $this->assertObjectHasAttribute('owner', $items['foo']); $this->assertObjectHasAttribute('order', $items['foo']); @@ -156,7 +156,7 @@ class NavigationManagerTest extends TestCase $this->assertEquals('October.Tester', $items['foo']->owner); $this->assertObjectHasAttribute('permissions', $items['foo']); - $this->assertInternalType('array', $items['foo']->permissions); + $this->assertIsArray($items['foo']->permissions); $this->assertCount(2, $items['foo']->permissions); $this->assertContains('october.tester.access_foo', $items['foo']->permissions); $this->assertContains('october.tester.access_bar', $items['foo']->permissions); diff --git a/tests/unit/backend/helpers/BackendHelperTest.php b/tests/unit/backend/helpers/BackendHelperTest.php index b4ee7c0c6..176690b78 100644 --- a/tests/unit/backend/helpers/BackendHelperTest.php +++ b/tests/unit/backend/helpers/BackendHelperTest.php @@ -11,8 +11,8 @@ class BackendHelperTest extends TestCase $assets = $backendHelper->decompileAsset('tests/fixtures/backend/assets/compilation.js'); $this->assertCount(2, $assets); - $this->assertContains('file1.js', $assets[0]); - $this->assertContains('file2.js', $assets[1]); + $this->assertStringContainsString('file1.js', $assets[0]); + $this->assertStringContainsString('file2.js', $assets[1]); } public function testDecompileMissingFile() diff --git a/tests/unit/backend/traits/WidgetMakerTest.php b/tests/unit/backend/traits/WidgetMakerTest.php index 1369a34d1..9a290b509 100644 --- a/tests/unit/backend/traits/WidgetMakerTest.php +++ b/tests/unit/backend/traits/WidgetMakerTest.php @@ -28,7 +28,7 @@ class WidgetMakerTest extends TestCase * * @return void */ - public function setUp() + public function setUp() : void { parent::setUp(); diff --git a/tests/unit/cms/classes/AssetTest.php b/tests/unit/cms/classes/AssetTest.php new file mode 100644 index 000000000..13519a161 --- /dev/null +++ b/tests/unit/cms/classes/AssetTest.php @@ -0,0 +1,119 @@ +assertStringContainsString( + 'console.log(\'script1.js\');', + Asset::load($theme, 'js/script1.js')->content + ); + + // Valid direct subdirectory path + $this->assertStringContainsString( + 'console.log(\'subdir/script1.js\');', + Asset::load($theme, 'js/subdir/script1.js')->content + ); + + // Valid relative path + $this->assertStringContainsString( + 'console.log(\'script2.js\');', + Asset::load($theme, 'js/subdir/../script2.js')->content + ); + + // Invalid theme path + $this->assertNull( + Asset::load($theme, 'js/invalid.js') + ); + + // Check that we cannot break out of assets directory + $this->assertNull( + Asset::load($theme, '../../../../js/helpers/fakeDom.js') + ); + $this->assertNull( + Asset::load($theme, '../content/html-content.htm') + ); + + // Check that we cannot load directories directly + $this->assertNull( + Asset::load($theme, 'js/subdir') + ); + + // Check that we definitely cannot load external PHP files + $this->assertNull( + Asset::load($theme, '../../../../../config/database.php') + ); + } + + public function testGetPath() + { + // Test some pathing fringe cases + + $theme = Theme::load('test'); + $assetClass = new Asset($theme); + $themeDir = $theme->getPath(); + + // Direct paths + $this->assertEquals( + $themeDir . '/assets/js/script1.js', + $assetClass->getFilePath('js/script1.js') + ); + $this->assertEquals( + $themeDir . '/assets/js/script1.js', + $assetClass->getFilePath('/js/script1.js') + ); + + // Direct path to a directory + $this->assertEquals( + $themeDir . '/assets/js/subdir', + $assetClass->getFilePath('/js/subdir') + ); + $this->assertEquals( + $themeDir . '/assets/js/subdir', + $assetClass->getFilePath('/js/subdir/') + ); + + // Relative paths + $this->assertEquals( + $themeDir . '/assets/js/script2.js', + $assetClass->getFilePath('./js/script2.js') + ); + $this->assertEquals( + $themeDir . '/assets/js/script2.js', + $assetClass->getFilePath('/js/subdir/../script2.js') + ); + + // Missing file, but valid directory (allows for new files) + $this->assertEquals( + $themeDir . '/assets/js/missing.js', + $assetClass->getFilePath('/js/missing.js') + ); + $this->assertEquals( + $themeDir . '/assets/js/missing.js', + $assetClass->getFilePath('js/missing.js') + ); + + // Missing file and missing directory (new directories are created as needed) + $this->assertEquals( + $themeDir . '/assets/js/missing/missing.js', + $assetClass->getFilePath('/js/missing/missing.js') + ); + + // Ensure we cannot get paths outside of the assets directory + $this->assertFalse( + $assetClass->getFilePath('../../../../js/helpers/fakeDom.js') + ); + $this->assertFalse( + $assetClass->getFilePath('../content/html-content.htm') + ); + $this->assertFalse( + $assetClass->getFilePath('../../../../../config/database.php') + ); + } +} diff --git a/tests/unit/cms/classes/CmsCompoundObjectTest.php b/tests/unit/cms/classes/CmsCompoundObjectTest.php index c66d31ee5..ec82153f9 100644 --- a/tests/unit/cms/classes/CmsCompoundObjectTest.php +++ b/tests/unit/cms/classes/CmsCompoundObjectTest.php @@ -30,7 +30,7 @@ class TestTemporaryCmsCompoundObject extends CmsCompoundObject class CmsCompoundObjectTest extends TestCase { - public function setUp() + public function setUp() : void { parent::setUp(); Model::clearBootedModels(); @@ -44,16 +44,16 @@ class CmsCompoundObjectTest extends TestCase $theme = Theme::load('test'); $obj = TestCmsCompoundObject::load($theme, 'compound.htm'); - $this->assertContains("\$controller->data['something'] = 'some value'", $obj->code); + $this->assertStringContainsString("\$controller->data['something'] = 'some value'", $obj->code); $this->assertEquals('

This is a paragraph

', $obj->markup); - $this->assertInternalType('array', $obj->settings); + $this->assertIsArray($obj->settings); $this->assertArrayHasKey('var', $obj->settings); $this->assertEquals('value', $obj->settings['var']); $this->assertArrayHasKey('components', $obj->settings); $this->assertArrayHasKey('section', $obj->settings['components']); - $this->assertInternalType('array', $obj->settings['components']['section']); + $this->assertIsArray($obj->settings['components']['section']); $this->assertArrayHasKey('version', $obj->settings['components']['section']); $this->assertEquals(10, $obj->settings['components']['section']['version']); @@ -69,7 +69,7 @@ class CmsCompoundObjectTest extends TestCase $obj = TestCmsCompoundObject::load($theme, 'component.htm'); $this->assertArrayHasKey('components', $obj->settings); - $this->assertInternalType('array', $obj->settings['components']); + $this->assertIsArray($obj->settings['components']); $this->assertArrayHasKey('testArchive', $obj->settings['components']); $this->assertArrayHasKey('posts-per-page', $obj->settings['components']['testArchive']); $this->assertEquals(10, $obj->settings['components']['testArchive']['posts-per-page']); @@ -82,7 +82,7 @@ class CmsCompoundObjectTest extends TestCase $obj = TestCmsCompoundObject::load($theme, 'components.htm'); $this->assertArrayHasKey('components', $obj->settings); - $this->assertInternalType('array', $obj->settings['components']); + $this->assertIsArray($obj->settings['components']); $this->assertArrayHasKey('testArchive firstAlias', $obj->settings['components']); $this->assertArrayHasKey('October\Tester\Components\Post secondAlias', $obj->settings['components']); @@ -108,7 +108,7 @@ class CmsCompoundObjectTest extends TestCase $properties = $obj->getComponentProperties('October\Tester\Components\Post'); $emptyProperties = $obj->getComponentProperties('October\Tester\Components\Archive'); $notExistingProperties = $obj->getComponentProperties('This\Is\Not\Component'); - $this->assertInternalType('array', $properties); + $this->assertIsArray($properties); $this->assertArrayHasKey('show-featured', $properties); $this->assertTrue((bool)$properties['show-featured']); $this->assertEquals('true', $properties['show-featured']); @@ -148,18 +148,18 @@ class CmsCompoundObjectTest extends TestCase $this->assertEquals($testContent, $obj->getContent()); $this->assertEquals('testcompound.htm', $obj->getFileName()); $this->assertEquals('

This is a paragraph

', $obj->markup); - $this->assertInternalType('array', $obj->settings); + $this->assertIsArray($obj->settings); $this->assertArrayHasKey('var', $obj->settings); $this->assertEquals('value', $obj->settings['var']); $this->assertArrayHasKey('components', $obj->settings); - $this->assertInternalType('array', $obj->settings['components']['section']); + $this->assertIsArray($obj->settings['components']['section']); $this->assertArrayHasKey('version', $obj->settings['components']['section']); $this->assertEquals(10, $obj->settings['components']['section']['version']); $this->assertEquals('value', $obj->var); - $this->assertInternalType('array', $obj->settings['components']['section']); + $this->assertIsArray($obj->settings['components']['section']); $this->assertArrayHasKey('version', $obj->settings['components']['section']); $this->assertEquals(10, $obj->settings['components']['section']['version']); @@ -173,18 +173,18 @@ class CmsCompoundObjectTest extends TestCase $this->assertEquals($testContent, $obj->getContent()); $this->assertEquals('testcompound.htm', $obj->getFileName()); $this->assertEquals('

This is a paragraph

', $obj->markup); - $this->assertInternalType('array', $obj->settings); + $this->assertIsArray($obj->settings); $this->assertArrayHasKey('var', $obj->settings); $this->assertEquals('value', $obj->settings['var']); $this->assertArrayHasKey('components', $obj->settings); - $this->assertInternalType('array', $obj->settings['components']['section']); + $this->assertIsArray($obj->settings['components']['section']); $this->assertArrayHasKey('version', $obj->settings['components']['section']); $this->assertEquals(10, $obj->settings['components']['section']['version']); $this->assertEquals('value', $obj->var); - $this->assertInternalType('array', $obj->settings['components']['section']); + $this->assertIsArray($obj->settings['components']['section']); $this->assertArrayHasKey('version', $obj->settings['components']['section']); $this->assertEquals(10, $obj->settings['components']['section']['version']); } @@ -280,14 +280,14 @@ class CmsCompoundObjectTest extends TestCase $obj = TestParsedCmsCompoundObject::load($theme, 'viewbag.htm'); $this->assertNull($obj->code); $this->assertEquals('

Chop Suey!

', $obj->markup); - $this->assertInternalType('array', $obj->settings); + $this->assertIsArray($obj->settings); $this->assertArrayHasKey('var', $obj->settings); $this->assertEquals('value', $obj->settings['var']); $this->assertArrayHasKey('components', $obj->settings); $this->assertArrayHasKey('viewBag', $obj->settings['components']); - $this->assertInternalType('array', $obj->settings['components']['viewBag']); + $this->assertIsArray($obj->settings['components']['viewBag']); $this->assertArrayHasKey('title', $obj->settings['components']['viewBag']); $this->assertEquals('Toxicity', $obj->settings['components']['viewBag']['title']); diff --git a/tests/unit/cms/classes/CmsObjectQueryTest.php b/tests/unit/cms/classes/CmsObjectQueryTest.php index 978745ae1..dc5172add 100644 --- a/tests/unit/cms/classes/CmsObjectQueryTest.php +++ b/tests/unit/cms/classes/CmsObjectQueryTest.php @@ -7,7 +7,7 @@ use October\Rain\Halcyon\Model; class CmsObjectQueryTest extends TestCase { - public function setUp() + public function setUp() : void { parent::setUp(); diff --git a/tests/unit/cms/classes/CmsObjectTest.php b/tests/unit/cms/classes/CmsObjectTest.php index 95d4f50d9..2c65761b4 100644 --- a/tests/unit/cms/classes/CmsObjectTest.php +++ b/tests/unit/cms/classes/CmsObjectTest.php @@ -148,12 +148,11 @@ class CmsObjectTest extends TestCase $this->assertNull($obj->something); } - /** - * @expectedException \October\Rain\Exception\ValidationException - * @expectedExceptionMessage Invalid file name - */ public function testFillInvalidFileNameSymbol() { + $this->expectException(\October\Rain\Exception\ValidationException::class); + $this->expectExceptionMessage('Invalid file name'); + $theme = Theme::load('apitest'); $testContents = 'mytestcontent'; @@ -164,12 +163,11 @@ class CmsObjectTest extends TestCase $obj->save(); } - /** - * @expectedException \October\Rain\Exception\ValidationException - * @expectedExceptionMessage Invalid file name - */ public function testFillInvalidFileNamePath() { + $this->expectException(\October\Rain\Exception\ValidationException::class); + $this->expectExceptionMessage('Invalid file name'); + $theme = Theme::load('apitest'); $testContents = 'mytestcontent'; @@ -180,12 +178,11 @@ class CmsObjectTest extends TestCase $obj->save(); } - /** - * @expectedException \October\Rain\Exception\ValidationException - * @expectedExceptionMessage Invalid file name - */ public function testFillInvalidFileSlash() { + $this->expectException(\October\Rain\Exception\ValidationException::class); + $this->expectExceptionMessage('Invalid file name'); + $theme = Theme::load('apitest'); $testContents = 'mytestcontent'; @@ -196,12 +193,11 @@ class CmsObjectTest extends TestCase $obj->save(); } - /** - * @expectedException \October\Rain\Exception\ValidationException - * @expectedExceptionMessage The File Name field is required - */ public function testFillEmptyFileName() { + $this->expectException(\October\Rain\Exception\ValidationException::class); + $this->expectExceptionMessage('The File Name field is required'); + $theme = Theme::load('apitest'); $testContents = 'mytestcontent'; @@ -266,11 +262,12 @@ class CmsObjectTest extends TestCase /** * @depends testRename - * @expectedException \October\Rain\Exception\ApplicationException - * @expectedExceptionMessage already exists */ public function testRenameToExistingFile() { + $this->expectException(\October\Rain\Exception\ApplicationException::class); + $this->expectExceptionMessageMatches('/already\sexists/'); + $theme = Theme::load('apitest'); $srcFilePath = $theme->getPath().'/testobjects/anotherobj.htm'; diff --git a/tests/unit/cms/classes/CodeParserTest.php b/tests/unit/cms/classes/CodeParserTest.php index 43eb34a84..67d8f30e9 100644 --- a/tests/unit/cms/classes/CodeParserTest.php +++ b/tests/unit/cms/classes/CodeParserTest.php @@ -10,7 +10,7 @@ use Cms\Classes\Controller; class CodeParserTest extends TestCase { - public function setUp() + public function setUp() : void { parent::setup(); @@ -41,7 +41,7 @@ class CodeParserTest extends TestCase $parser = new CodeParser($layout); $info = $parser->parse(); - $this->assertInternalType('array', $info); + $this->assertIsArray($info); $this->assertArrayHasKey('filePath', $info); $this->assertArrayHasKey('className', $info); $this->assertArrayHasKey('source', $info); @@ -78,7 +78,7 @@ class CodeParserTest extends TestCase $parser = new CodeParser($layout); $info = $parser->parse(); - $this->assertInternalType('array', $info); + $this->assertIsArray($info); $this->assertEquals('request-cache', $info['source']); $this->assertFileExists($info['filePath']); @@ -91,7 +91,7 @@ class CodeParserTest extends TestCase $parser = new CodeParser($layout); $info = $parser->parse(); - $this->assertInternalType('array', $info); + $this->assertIsArray($info); $this->assertEquals('cache', $info['source']); $this->assertFileExists($info['filePath']); @@ -101,7 +101,7 @@ class CodeParserTest extends TestCase $parser = new CodeParser($layout); $info = $parser->parse(); - $this->assertInternalType('array', $info); + $this->assertIsArray($info); $this->assertEquals('request-cache', $info['source']); $this->assertFileExists($info['filePath']); @@ -110,13 +110,14 @@ class CodeParserTest extends TestCase */ $this->assertTrue(@touch($layout->getFilePath())); + clearstatcache(); $layout = Layout::load($theme, 'php-parser-test.htm'); $this->assertNotEmpty($layout); $parser = new CodeParser($layout); $property->setValue($parser, []); $info = $parser->parse(); - $this->assertInternalType('array', $info); + $this->assertIsArray($info); $this->assertEquals('parser', $info['source']); $this->assertFileExists($info['filePath']); } @@ -131,7 +132,7 @@ class CodeParserTest extends TestCase $parser = new CodeParser($layout); $info = $parser->parse(); - $this->assertInternalType('array', $info); + $this->assertIsArray($info); $this->assertArrayHasKey('filePath', $info); $this->assertArrayHasKey('className', $info); $this->assertArrayHasKey('source', $info); @@ -157,7 +158,7 @@ class CodeParserTest extends TestCase $parser = new CodeParser($page); $info = $parser->parse(); - $this->assertInternalType('array', $info); + $this->assertIsArray($info); $this->assertArrayHasKey('filePath', $info); $this->assertArrayHasKey('className', $info); $this->assertArrayHasKey('source', $info); @@ -191,7 +192,7 @@ class CodeParserTest extends TestCase $parser = new CodeParser($page); $info = $parser->parse(); - $this->assertInternalType('array', $info); + $this->assertIsArray($info); $this->assertArrayHasKey('filePath', $info); $this->assertArrayHasKey('className', $info); $this->assertArrayHasKey('source', $info); @@ -220,7 +221,7 @@ class CodeParserTest extends TestCase $parser = new CodeParser($page); $info = $parser->parse(); - $this->assertInternalType('array', $info); + $this->assertIsArray($info); $this->assertArrayHasKey('filePath', $info); $this->assertArrayHasKey('className', $info); $this->assertArrayHasKey('source', $info); @@ -255,7 +256,7 @@ class CodeParserTest extends TestCase $parser = new CodeParser($page); $info = $parser->parse(); - $this->assertInternalType('array', $info); + $this->assertIsArray($info); $this->assertArrayHasKey('filePath', $info); $this->assertArrayHasKey('className', $info); $this->assertArrayHasKey('source', $info); @@ -284,7 +285,7 @@ class CodeParserTest extends TestCase $parser = new CodeParser($page); $info = $parser->parse(); - $this->assertInternalType('array', $info); + $this->assertIsArray($info); $this->assertArrayHasKey('filePath', $info); $this->assertArrayHasKey('className', $info); $this->assertArrayHasKey('source', $info); diff --git a/tests/unit/cms/classes/ComponentManagerTest.php b/tests/unit/cms/classes/ComponentManagerTest.php index a12255580..152367b8d 100644 --- a/tests/unit/cms/classes/ComponentManagerTest.php +++ b/tests/unit/cms/classes/ComponentManagerTest.php @@ -9,7 +9,7 @@ use Cms\Classes\ComponentManager; class ComponentManagerTest extends TestCase { - public function setUp() + public function setUp() : void { parent::setUp(); diff --git a/tests/unit/cms/classes/ControllerTest.php b/tests/unit/cms/classes/ControllerTest.php index 02b2abc34..1496055a3 100644 --- a/tests/unit/cms/classes/ControllerTest.php +++ b/tests/unit/cms/classes/ControllerTest.php @@ -6,7 +6,7 @@ use October\Rain\Halcyon\Model; class ControllerTest extends TestCase { - public function setUp() + public function setUp() : void { parent::setUp(); @@ -69,7 +69,7 @@ class ControllerTest extends TestCase $response = $controller->run('/some-page-that-doesnt-exist'); $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response); $content = $response->getContent(); - $this->assertInternalType('string', $content); + $this->assertIsString($content); $this->assertEquals('

Page not found

', $content); } @@ -83,16 +83,15 @@ class ControllerTest extends TestCase $response = $controller->run('/'); $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response); $content = $response->getContent(); - $this->assertInternalType('string', $content); + $this->assertIsString($content); $this->assertEquals('

My Webpage

', trim($content)); } - /** - * @expectedException Cms\Classes\CmsException - * @expectedExceptionMessage is not found - */ public function testLayoutNotFound() { + $this->expectException(\Cms\Classes\CmsException::class); + $this->expectExceptionMessageMatches('/is\snot\sfound/'); + $theme = Theme::load('test'); $controller = new Controller($theme); $response = $controller->run('/no-layout'); @@ -146,12 +145,11 @@ class ControllerTest extends TestCase $this->assertEquals("
LAYOUT CONTENT

This page is a subdirectory

", $response); } - /** - * @expectedException \Twig\Error\RuntimeError - * @expectedExceptionMessage is not found - */ public function testPartialNotFound() { + $this->expectException(\Twig\Error\RuntimeError::class); + $this->expectExceptionMessageMatches('/is\snot\sfound/'); + $theme = Theme::load('test'); $controller = new Controller($theme); $response = $controller->run('/no-partial')->getContent(); @@ -193,12 +191,11 @@ class ControllerTest extends TestCase return $requestMock; } - /** - * @expectedException Cms\Classes\CmsException - * @expectedExceptionMessage AJAX handler 'onNoHandler' was not found. - */ public function testAjaxHandlerNotFound() { + $this->expectException(\Cms\Classes\CmsException::class); + $this->expectExceptionMessage('AJAX handler \'onNoHandler\' was not found.'); + Request::swap($this->configAjaxRequestMock('onNoHandler', '')); $theme = Theme::load('test'); @@ -206,12 +203,11 @@ class ControllerTest extends TestCase $controller->run('/ajax-test'); } - /** - * @expectedException Cms\Classes\CmsException - * @expectedExceptionMessage Invalid AJAX handler name: delete. - */ public function testAjaxInvalidHandlerName() { + $this->expectException(\Cms\Classes\CmsException::class); + $this->expectExceptionMessage('Invalid AJAX handler name: delete.'); + Request::swap($this->configAjaxRequestMock('delete')); $theme = Theme::load('test'); @@ -219,12 +215,11 @@ class ControllerTest extends TestCase $controller->run('/ajax-test'); } - /** - * @expectedException Cms\Classes\CmsException - * @expectedExceptionMessage Invalid partial name: p:artial. - */ public function testAjaxInvalidPartial() { + $this->expectException(\Cms\Classes\CmsException::class); + $this->expectExceptionMessage('Invalid partial name: p:artial.'); + Request::swap($this->configAjaxRequestMock('onTest', 'p:artial')); $theme = Theme::load('test'); @@ -232,12 +227,11 @@ class ControllerTest extends TestCase $controller->run('/ajax-test'); } - /** - * @expectedException Cms\Classes\CmsException - * @expectedExceptionMessage The partial 'partial' is not found. - */ public function testAjaxPartialNotFound() { + $this->expectException(\Cms\Classes\CmsException::class); + $this->expectExceptionMessage('The partial \'partial\' is not found.'); + Request::swap($this->configAjaxRequestMock('onTest', 'partial')); $theme = Theme::load('test'); @@ -255,7 +249,7 @@ class ControllerTest extends TestCase $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response); $content = $response->getOriginalContent(); - $this->assertInternalType('array', $content); + $this->assertIsArray($content); $this->assertEquals(200, $response->getStatusCode()); $this->assertCount(1, $content); $this->assertArrayHasKey('ajax-result', $content); @@ -272,7 +266,7 @@ class ControllerTest extends TestCase $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response); $content = $response->getOriginalContent(); - $this->assertInternalType('array', $content); + $this->assertIsArray($content); $this->assertEquals(200, $response->getStatusCode()); $this->assertCount(1, $content); $this->assertArrayHasKey('ajax-result', $content); @@ -289,7 +283,7 @@ class ControllerTest extends TestCase $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response); $content = $response->getOriginalContent(); - $this->assertInternalType('array', $content); + $this->assertIsArray($content); $this->assertEquals(200, $response->getStatusCode()); $this->assertCount(2, $content); $this->assertArrayHasKey('ajax-result', $content); @@ -303,7 +297,7 @@ class ControllerTest extends TestCase $theme = Theme::load('test'); $controller = new Controller($theme); $response = $controller->run('/with-component')->getContent(); - $page = $this->readAttribute($controller, 'page'); + $page = self::getProtectedProperty($controller, 'page'); $this->assertArrayHasKey('testArchive', $page->components); $component = $page->components['testArchive']; @@ -331,7 +325,7 @@ ESC; $theme = Theme::load('test'); $controller = new Controller($theme); $response = $controller->run('/with-components')->getContent(); - $page = $this->readAttribute($controller, 'page'); + $page = self::getProtectedProperty($controller, 'page'); $this->assertArrayHasKey('firstAlias', $page->components); $this->assertArrayHasKey('secondAlias', $page->components); @@ -363,19 +357,18 @@ ESC; $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response); $content = $response->getOriginalContent(); - $this->assertInternalType('array', $content); + $this->assertIsArray($content); $this->assertEquals(200, $response->getStatusCode()); $this->assertCount(1, $content); $this->assertArrayHasKey('ajax-result', $content); $this->assertEquals('page', $content['ajax-result']); } - /** - * @expectedException October\Rain\Exception\SystemException - * @expectedExceptionMessage is not registered for the component - */ public function testComponentClassNotFound() { + $this->expectException(\October\Rain\Exception\SystemException::class); + $this->expectExceptionMessageMatches('/is\snot\sregistered\sfor\sthe\scomponent/'); + $theme = Theme::load('test'); $controller = new Controller($theme); $response = $controller->run('/no-component-class')->getContent(); @@ -395,7 +388,7 @@ ESC; $theme = Theme::load('test'); $controller = new Controller($theme); $response = $controller->run('/with-soft-component-class')->getContent(); - $page = $this->readAttribute($controller, 'page'); + $page = $controller->getPage(); $this->assertArrayHasKey('testArchive', $page->components); $component = $page->components['testArchive']; @@ -421,7 +414,7 @@ ESC; $theme = Theme::load('test'); $controller = new Controller($theme); $response = $controller->run('/with-soft-component-class-alias')->getContent(); - $page = $this->readAttribute($controller, 'page'); + $page = $controller->getPage(); $this->assertArrayHasKey('someAlias', $page->components); $component = $page->components['someAlias']; diff --git a/tests/unit/cms/classes/RouterTest.php b/tests/unit/cms/classes/RouterTest.php index 9bbcd3247..f4eeda27a 100644 --- a/tests/unit/cms/classes/RouterTest.php +++ b/tests/unit/cms/classes/RouterTest.php @@ -7,7 +7,7 @@ class RouterTest extends TestCase { protected static $theme = null; - public function setUp() + public function setUp() : void { parent::setUp(); @@ -43,7 +43,7 @@ class RouterTest extends TestCase $this->assertFalse($value); $map = $property->getValue($router); - $this->assertInternalType('array', $map); + $this->assertIsArray($map); $this->assertGreaterThanOrEqual(4, count($map)); /* @@ -52,7 +52,7 @@ class RouterTest extends TestCase $value = $method->invoke($router); $this->assertTrue($value); $map = $property->getValue($router); - $this->assertInternalType('array', $map); + $this->assertIsArray($map); $this->assertGreaterThanOrEqual(4, count($map)); } diff --git a/tests/unit/cms/classes/ThemeTest.php b/tests/unit/cms/classes/ThemeTest.php index a45087ede..07a15f2db 100644 --- a/tests/unit/cms/classes/ThemeTest.php +++ b/tests/unit/cms/classes/ThemeTest.php @@ -4,7 +4,7 @@ use Cms\Classes\Theme; class ThemeTest extends TestCase { - public function setUp() + public function setUp() : void { parent::setUp(); @@ -44,7 +44,7 @@ class ThemeTest extends TestCase $pageCollection = $theme->listPages(); $pages = array_values($pageCollection->all()); - $this->assertInternalType('array', $pages); + $this->assertIsArray($pages); $expectedPageNum = $this->countThemePages(base_path().'/tests/fixtures/themes/test/pages'); $this->assertCount($expectedPageNum, $pages); @@ -63,12 +63,11 @@ class ThemeTest extends TestCase $this->assertEquals('test', $activeTheme->getDirName()); } - /** - * @expectedException \October\Rain\Exception\SystemException - * @expectedExceptionMessage The active theme is not set. - */ public function testNoActiveTheme() { + $this->expectException(\October\Rain\Exception\SystemException::class); + $this->expectExceptionMessage('The active theme is not set.'); + Config::set('cms.activeTheme', null); Theme::getActiveTheme(); } diff --git a/tests/unit/plugins/database/AttachManyModelTest.php b/tests/unit/plugins/database/AttachManyModelTest.php index 79c03c443..9c316c032 100644 --- a/tests/unit/plugins/database/AttachManyModelTest.php +++ b/tests/unit/plugins/database/AttachManyModelTest.php @@ -5,7 +5,7 @@ use Database\Tester\Models\User; class AttachManyModelTest extends PluginTestCase { - public function setUp() + public function setUp() : void { parent::setUp(); diff --git a/tests/unit/plugins/database/AttachOneModelTest.php b/tests/unit/plugins/database/AttachOneModelTest.php index 52cb62fdf..d50debdb6 100644 --- a/tests/unit/plugins/database/AttachOneModelTest.php +++ b/tests/unit/plugins/database/AttachOneModelTest.php @@ -7,7 +7,7 @@ use Symfony\Component\HttpFoundation\File\UploadedFile; class AttachOneModelTest extends PluginTestCase { - public function setUp() + public function setUp() : void { parent::setUp(); diff --git a/tests/unit/plugins/database/BelongsToManyModelTest.php b/tests/unit/plugins/database/BelongsToManyModelTest.php index e151d2304..e850b7752 100644 --- a/tests/unit/plugins/database/BelongsToManyModelTest.php +++ b/tests/unit/plugins/database/BelongsToManyModelTest.php @@ -5,7 +5,7 @@ use Database\Tester\Models\Author; class BelongsToManyModelTest extends PluginTestCase { - public function setUp() + public function setUp() : void { parent::setUp(); diff --git a/tests/unit/plugins/database/BelongsToModelTest.php b/tests/unit/plugins/database/BelongsToModelTest.php index 6d87dbd3a..8fca6ff06 100644 --- a/tests/unit/plugins/database/BelongsToModelTest.php +++ b/tests/unit/plugins/database/BelongsToModelTest.php @@ -5,7 +5,7 @@ use Database\Tester\Models\Author; class BelongsToModelTest extends PluginTestCase { - public function setUp() + public function setUp() : void { parent::setUp(); diff --git a/tests/unit/plugins/database/DeferredBindingTest.php b/tests/unit/plugins/database/DeferredBindingTest.php index a8f354a65..e36460255 100644 --- a/tests/unit/plugins/database/DeferredBindingTest.php +++ b/tests/unit/plugins/database/DeferredBindingTest.php @@ -6,7 +6,7 @@ use October\Rain\Database\Models\DeferredBinding; class DeferredBindingTest extends PluginTestCase { - public function setUp() + public function setUp() : void { parent::setUp(); diff --git a/tests/unit/plugins/database/HasManyModelTest.php b/tests/unit/plugins/database/HasManyModelTest.php index 64559dbc1..9a1def095 100644 --- a/tests/unit/plugins/database/HasManyModelTest.php +++ b/tests/unit/plugins/database/HasManyModelTest.php @@ -6,7 +6,7 @@ use October\Rain\Database\Collection; class HasManyModelTest extends PluginTestCase { - public function setUp() + public function setUp() : void { parent::setUp(); @@ -63,8 +63,6 @@ class HasManyModelTest extends PluginTestCase public function testGetRelationValue() { - $this->markTestSkipped('Marked as \'skipped\' for further investigation'); - Model::unguard(); $author = Author::create(['name' => 'Stevie']); $post1 = Post::create(['title' => "First post", 'author_id' => $author->id]); diff --git a/tests/unit/plugins/database/HasManyThroughModelTest.php b/tests/unit/plugins/database/HasManyThroughModelTest.php new file mode 100644 index 000000000..85ac947f2 --- /dev/null +++ b/tests/unit/plugins/database/HasManyThroughModelTest.php @@ -0,0 +1,54 @@ +runPluginRefreshCommand('Database.Tester'); + } + + public function testGet() + { + Model::unguard(); + $country = Country::create(['name' => 'Australia']); + $author1 = Author::create(['name' => 'Stevie', 'email' => 'stevie@email.tld']); + $author2 = Author::create(['name' => 'Louie', 'email' => 'louie@email.tld']); + $post1 = Post::create(['title' => "First post", 'description' => "Yay!!"]); + $post2 = Post::create(['title' => "Second post", 'description' => "Woohoo!!"]); + $post3 = Post::create(['title' => "Third post", 'description' => "Yipiee!!"]); + $post4 = Post::make(['title' => "Fourth post", 'description' => "Hooray!!"]); + Model::reguard(); + + // Set data + $author1->country = $country; + $author2->country = $country; + + $author1->posts = new Collection([$post1, $post2]); + $author2->posts = new Collection([$post3, $post4]); + + $author1->save(); + $author2->save(); + + $country = Country::with([ + 'posts' + ])->find($country->id); + + $this->assertEquals([ + $post1->id, + $post2->id, + $post3->id, + $post4->id + ], $country->posts->pluck('id')->toArray()); + } +} diff --git a/tests/unit/plugins/database/HasOneModelTest.php b/tests/unit/plugins/database/HasOneModelTest.php index 7a5e30157..e6bd0f45e 100644 --- a/tests/unit/plugins/database/HasOneModelTest.php +++ b/tests/unit/plugins/database/HasOneModelTest.php @@ -5,7 +5,7 @@ use Database\Tester\Models\Phone; class HasOneModelTest extends PluginTestCase { - public function setUp() + public function setUp() : void { parent::setUp(); diff --git a/tests/unit/plugins/database/HasOneThroughModelTest.php b/tests/unit/plugins/database/HasOneThroughModelTest.php new file mode 100644 index 000000000..91be83b60 --- /dev/null +++ b/tests/unit/plugins/database/HasOneThroughModelTest.php @@ -0,0 +1,39 @@ +runPluginRefreshCommand('Database.Tester'); + } + + public function testGet() + { + Model::unguard(); + $phone = Phone::create(['number' => '08 1234 5678']); + $author = Author::create(['name' => 'Stevie', 'email' => 'stevie@email.tld']); + $user = User::create(['name' => 'Stevie', 'email' => 'stevie@email.tld']); + Model::reguard(); + + // Set data + $author->phone = $phone; + $author->user = $user; + $author->save(); + + $user = User::with([ + 'phone' + ])->find($user->id); + + $this->assertEquals($phone->id, $user->phone->id); + } +} diff --git a/tests/unit/plugins/database/ModelTest.php b/tests/unit/plugins/database/ModelTest.php index 492b39505..16bf07c1d 100644 --- a/tests/unit/plugins/database/ModelTest.php +++ b/tests/unit/plugins/database/ModelTest.php @@ -4,7 +4,7 @@ use Database\Tester\Models\Post; class ModelTest extends PluginTestCase { - public function setUp() + public function setUp() : void { parent::setUp(); @@ -23,12 +23,11 @@ class ModelTest extends PluginTestCase $this->assertEquals(1, $post->id); } - /** - * @expectedException \Illuminate\Database\Eloquent\MassAssignmentException - * @expectedExceptionMessage title - */ public function testGuardedAttribute() { + $this->expectException(\Illuminate\Database\Eloquent\MassAssignmentException::class); + $this->expectExceptionMessageMatches('/title/'); + Post::create(['title' => 'Hi!', 'slug' => 'authenticity']); } } diff --git a/tests/unit/plugins/database/MorphManyModelTest.php b/tests/unit/plugins/database/MorphManyModelTest.php index e9b77aced..a7ba3041c 100644 --- a/tests/unit/plugins/database/MorphManyModelTest.php +++ b/tests/unit/plugins/database/MorphManyModelTest.php @@ -6,7 +6,7 @@ use October\Rain\Database\Collection; class MorphManyModelTest extends PluginTestCase { - public function setUp() + public function setUp() : void { parent::setUp(); diff --git a/tests/unit/plugins/database/MorphOneModelTest.php b/tests/unit/plugins/database/MorphOneModelTest.php index 4051e0b0c..e8c6f60fd 100644 --- a/tests/unit/plugins/database/MorphOneModelTest.php +++ b/tests/unit/plugins/database/MorphOneModelTest.php @@ -6,7 +6,7 @@ use Database\Tester\Models\Meta; class MorphOneModelTest extends PluginTestCase { - public function setUp() + public function setUp() : void { parent::setUp(); diff --git a/tests/unit/plugins/database/MorphToModelTest.php b/tests/unit/plugins/database/MorphToModelTest.php index 962f2d658..b0eb37ff7 100644 --- a/tests/unit/plugins/database/MorphToModelTest.php +++ b/tests/unit/plugins/database/MorphToModelTest.php @@ -6,7 +6,7 @@ use Database\Tester\Models\EventLog; class MorphToModelTest extends PluginTestCase { - public function setUp() + public function setUp() : void { parent::setUp(); diff --git a/tests/unit/plugins/database/NestedTreeModelTest.php b/tests/unit/plugins/database/NestedTreeModelTest.php index b2f0ae019..49d46764c 100644 --- a/tests/unit/plugins/database/NestedTreeModelTest.php +++ b/tests/unit/plugins/database/NestedTreeModelTest.php @@ -5,7 +5,7 @@ use Database\Tester\Models\CategoryNested; class NestedTreeModelTest extends PluginTestCase { - public function setUp() + public function setUp() : void { parent::setUp(); diff --git a/tests/unit/plugins/database/NullableModelTest.php b/tests/unit/plugins/database/NullableModelTest.php index 0484409d4..9d6d61114 100644 --- a/tests/unit/plugins/database/NullableModelTest.php +++ b/tests/unit/plugins/database/NullableModelTest.php @@ -4,7 +4,7 @@ use Database\Tester\Models\NullablePost; class NullableModelTest extends PluginTestCase { - public function setUp() + public function setUp() : void { parent::setUp(); diff --git a/tests/unit/plugins/database/RevisionableModelTest.php b/tests/unit/plugins/database/RevisionableModelTest.php index afee0c93d..c8fa19a54 100644 --- a/tests/unit/plugins/database/RevisionableModelTest.php +++ b/tests/unit/plugins/database/RevisionableModelTest.php @@ -5,7 +5,7 @@ use Database\Tester\Models\RevisionablePost; class RevisionableModelTest extends PluginTestCase { - public function setUp() + public function setUp() : void { parent::setUp(); diff --git a/tests/unit/plugins/database/SimpleTreeModelTest.php b/tests/unit/plugins/database/SimpleTreeModelTest.php index fac6a48f3..c22b11d16 100644 --- a/tests/unit/plugins/database/SimpleTreeModelTest.php +++ b/tests/unit/plugins/database/SimpleTreeModelTest.php @@ -5,7 +5,7 @@ use Database\Tester\Models\CategorySimple; class SimpleTreeModelTest extends PluginTestCase { - public function setUp() + public function setUp() : void { parent::setUp(); @@ -165,12 +165,11 @@ class SimpleTreeModelTest extends PluginTestCase ], $array); } - /** - * @expectedException \Exception - * @expectedExceptionMessage Column mismatch in listsNested method - */ public function testListsNestedUnknownColumn() { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Column mismatch in listsNested method'); + CategorySimple::listsNested('custom_name', 'id'); } diff --git a/tests/unit/plugins/database/SluggableModelTest.php b/tests/unit/plugins/database/SluggableModelTest.php index f1f7aac71..22cad1837 100644 --- a/tests/unit/plugins/database/SluggableModelTest.php +++ b/tests/unit/plugins/database/SluggableModelTest.php @@ -4,7 +4,7 @@ use Database\Tester\Models\SluggablePost; class SluggableModelTest extends PluginTestCase { - public function setUp() + public function setUp() : void { parent::setUp(); diff --git a/tests/unit/plugins/database/SoftDeleteModelTest.php b/tests/unit/plugins/database/SoftDeleteModelTest.php index 23a0ef436..fc5212433 100644 --- a/tests/unit/plugins/database/SoftDeleteModelTest.php +++ b/tests/unit/plugins/database/SoftDeleteModelTest.php @@ -10,7 +10,7 @@ use Database\Tester\Models\UserWithSoftAuthorAndSoftDelete; class SoftDeleteModelTest extends PluginTestCase { - public function setUp() + public function setUp() : void { parent::setUp(); diff --git a/tests/unit/plugins/database/ValidationModelTest.php b/tests/unit/plugins/database/ValidationModelTest.php index ce1f4e382..f5ddb55b7 100644 --- a/tests/unit/plugins/database/ValidationModelTest.php +++ b/tests/unit/plugins/database/ValidationModelTest.php @@ -4,7 +4,7 @@ use Database\Tester\Models\ValidationPost; class ValidationModelTest extends PluginTestCase { - public function setUp() + public function setUp() : void { parent::setUp(); @@ -13,11 +13,10 @@ class ValidationModelTest extends PluginTestCase $this->runPluginRefreshCommand('Database.Tester'); } - /** - * @expectedException October\Rain\Database\ModelException - */ public function testUniqueTableValidation() { + $this->expectException(\October\Rain\Database\ModelException::class); + $post = ValidationPost::create([ 'title' => 'This is a new post', 'slug' => 'post-1', diff --git a/tests/unit/system/AliasesTest.php b/tests/unit/system/AliasesTest.php new file mode 100644 index 000000000..8d389cdcc --- /dev/null +++ b/tests/unit/system/AliasesTest.php @@ -0,0 +1,22 @@ +assertTrue(class_exists('Illuminate\Support\Facades\Input')); + $this->assertInstanceOf( + \October\Rain\Support\Facades\Input::class, + new \Illuminate\Support\Facades\Input() + ); + } + + public function testHtmlDumperAlias() + { + $this->assertTrue(class_exists('Illuminate\Support\Debug\HtmlDumper')); + $this->assertInstanceOf( + \Symfony\Component\VarDumper\Dumper\HtmlDumper::class, + new \Illuminate\Support\Debug\HtmlDumper() + ); + } +} diff --git a/tests/unit/system/classes/AutoDatasourceTest.php b/tests/unit/system/classes/AutoDatasourceTest.php index 43410cc77..14dc6f54b 100644 --- a/tests/unit/system/classes/AutoDatasourceTest.php +++ b/tests/unit/system/classes/AutoDatasourceTest.php @@ -7,7 +7,7 @@ use October\Rain\Halcyon\Datasource\FileDatasource; class CmsThemeTemplateFixture extends Model { - protected $fillable = ['*']; + protected $guarded = []; public $timestamps = false; @@ -30,7 +30,7 @@ class AutoDatasourceTest extends PluginTestCase */ public $datasource; - public function setUp() + public function setUp(): void { parent::setUp(); @@ -73,7 +73,7 @@ class AutoDatasourceTest extends PluginTestCase ]); } - public function tearDown() + public function tearDown(): void { foreach ($this->fixtures as $fixture) { $fixture->delete(); diff --git a/tests/unit/system/classes/CombineAssetsTest.php b/tests/unit/system/classes/CombineAssetsTest.php index c9fed4a04..5c5d313bd 100644 --- a/tests/unit/system/classes/CombineAssetsTest.php +++ b/tests/unit/system/classes/CombineAssetsTest.php @@ -5,7 +5,7 @@ use System\Classes\CombineAssets; class CombineAssetsTest extends TestCase { - public function setUp() + public function setUp() : void { parent::setUp(); @@ -24,10 +24,10 @@ class CombineAssetsTest extends TestCase * Supported file extensions should exist */ $jsExt = $cssExt = self::getProtectedProperty($combiner, 'jsExtensions'); - $this->assertInternalType('array', $jsExt); + $this->assertIsArray($jsExt); $cssExt = self::getProtectedProperty($combiner, 'cssExtensions'); - $this->assertInternalType('array', $cssExt); + $this->assertIsArray($cssExt); /* * Check service methods diff --git a/tests/unit/system/classes/MarkupManagerTest.php b/tests/unit/system/classes/MarkupManagerTest.php index 48a82ba3e..f4ee00a32 100644 --- a/tests/unit/system/classes/MarkupManagerTest.php +++ b/tests/unit/system/classes/MarkupManagerTest.php @@ -5,7 +5,7 @@ use System\Classes\MarkupManager; class MarkupManagerTest extends TestCase { - public function setUp() + public function setUp() : void { parent::setUp(); diff --git a/tests/unit/system/classes/MediaLibraryTest.php b/tests/unit/system/classes/MediaLibraryTest.php index 35e561594..54c4a6a3e 100644 --- a/tests/unit/system/classes/MediaLibraryTest.php +++ b/tests/unit/system/classes/MediaLibraryTest.php @@ -4,7 +4,7 @@ use System\Classes\MediaLibrary; class MediaLibraryTest extends TestCase // @codingStandardsIgnoreLine { - protected function tearDown() + protected function tearDown() : void { $this->removeMedia(); parent::tearDown(); @@ -66,7 +66,7 @@ class MediaLibraryTest extends TestCase // @codingStandardsIgnoreLine public function testValidPathsOnValidatePath($path) { $result = MediaLibrary::validatePath($path); - $this->assertInternalType('string', $result); + $this->assertIsString($result); } public function testListFolderContents() diff --git a/tests/unit/system/classes/PluginManagerTest.php b/tests/unit/system/classes/PluginManagerTest.php index 4b20f8509..c963f2dd6 100644 --- a/tests/unit/system/classes/PluginManagerTest.php +++ b/tests/unit/system/classes/PluginManagerTest.php @@ -5,7 +5,7 @@ class PluginManagerTest extends TestCase { public $manager; - public function setUp() + public function setUp() : void { parent::setUp(); diff --git a/tests/unit/system/classes/VersionManagerTest.php b/tests/unit/system/classes/VersionManagerTest.php index b9934d3e5..ba163d18c 100644 --- a/tests/unit/system/classes/VersionManagerTest.php +++ b/tests/unit/system/classes/VersionManagerTest.php @@ -5,7 +5,7 @@ use System\Classes\VersionManager; class VersionManagerTest extends TestCase { - public function setUp() + public function setUp() : void { parent::setUp(); @@ -100,8 +100,8 @@ class VersionManagerTest extends TestCase $manager = VersionManager::instance(); list($comments, $scripts) = self::callProtectedMethod($manager, 'extractScriptsAndComments', [$versionInfo]); - $this->assertInternalType('array', $comments); - $this->assertInternalType('array', $scripts); + $this->assertIsArray($comments); + $this->assertIsArray($scripts); $this->assertEquals($expectedComments, $comments); $this->assertEquals($expectedScripts, $scripts); diff --git a/tests/unit/system/console/OctoberEnvTest.php b/tests/unit/system/console/OctoberEnvTest.php index 6170cc739..184c3e68d 100644 --- a/tests/unit/system/console/OctoberEnvTest.php +++ b/tests/unit/system/console/OctoberEnvTest.php @@ -13,7 +13,7 @@ class OctoberEnvTest extends TestCase /** @var string Stores the original config path from the app container */ public static $origConfigPath; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -30,59 +30,32 @@ class OctoberEnvTest extends TestCase // Check environment file $envFile = file_get_contents(base_path('.env')); - // Forward compatible assertions - // @TODO: Use only `assertStringContainsString` after L6 upgrade - - if (method_exists($this, 'assertStringContainsString')) { - $this->assertStringContainsString('APP_DEBUG=true', $envFile); - $this->assertStringContainsString('APP_URL=https://localhost', $envFile); - $this->assertStringContainsString('DB_CONNECTION=mysql', $envFile); - $this->assertStringContainsString('DB_DATABASE="data#base"', $envFile); - $this->assertStringContainsString('DB_USERNAME="teal\'c"', $envFile); - $this->assertStringContainsString('DB_PASSWORD="test\\"quotes\'test"', $envFile); - $this->assertStringContainsString('DB_PORT=3306', $envFile); - } else { - $this->assertContains('APP_DEBUG=true', $envFile); - $this->assertContains('APP_URL=https://localhost', $envFile); - $this->assertContains('DB_CONNECTION=mysql', $envFile); - $this->assertContains('DB_DATABASE="data#base"', $envFile); - $this->assertContains('DB_USERNAME="teal\'c"', $envFile); - $this->assertContains('DB_PASSWORD="test\\"quotes\'test"', $envFile); - $this->assertContains('DB_PORT=3306', $envFile); - } + $this->assertStringContainsString('APP_DEBUG=true', $envFile); + $this->assertStringContainsString('APP_URL=https://localhost', $envFile); + $this->assertStringContainsString('DB_CONNECTION=mysql', $envFile); + $this->assertStringContainsString('DB_DATABASE="data#base"', $envFile); + $this->assertStringContainsString('DB_USERNAME="teal\'c"', $envFile); + $this->assertStringContainsString('DB_PASSWORD="test\\"quotes\'test"', $envFile); + $this->assertStringContainsString('DB_PORT=3306', $envFile); // Check app.php config file $appConfigFile = file_get_contents(storage_path('temp/tests/config/app.php')); - if (method_exists($this, 'assertStringContainsString')) { - $this->assertStringContainsString('\'debug\' => env(\'APP_DEBUG\', true),', $appConfigFile); - $this->assertStringContainsString('\'url\' => env(\'APP_URL\', \'https://localhost\'),', $appConfigFile); - } else { - $this->assertContains('\'debug\' => env(\'APP_DEBUG\', true),', $appConfigFile); - $this->assertContains('\'url\' => env(\'APP_URL\', \'https://localhost\'),', $appConfigFile); - } + $this->assertStringContainsString('\'debug\' => env(\'APP_DEBUG\', true),', $appConfigFile); + $this->assertStringContainsString('\'url\' => env(\'APP_URL\', \'https://localhost\'),', $appConfigFile); // Check database.php config file $appConfigFile = file_get_contents(storage_path('temp/tests/config/database.php')); - if (method_exists($this, 'assertStringContainsString')) { - $this->assertStringContainsString('\'default\' => env(\'DB_CONNECTION\', \'mysql\')', $appConfigFile); - $this->assertStringContainsString('\'port\' => env(\'DB_PORT\', 3306),', $appConfigFile); - // Both the following configurations had values in the original config, they should be stripped out once - // the .env file is generated. - $this->assertStringContainsString('\'username\' => env(\'DB_USERNAME\', \'\'),', $appConfigFile); - $this->assertStringContainsString('\'password\' => env(\'DB_PASSWORD\', \'\'),', $appConfigFile); - } else { - $this->assertContains('\'default\' => env(\'DB_CONNECTION\', \'mysql\')', $appConfigFile); - $this->assertContains('\'port\' => env(\'DB_PORT\', 3306),', $appConfigFile); - // Both the following configurations had values in the original config, they should be stripped out once - // the .env file is generated. - $this->assertContains('\'username\' => env(\'DB_USERNAME\', \'\'),', $appConfigFile); - $this->assertContains('\'password\' => env(\'DB_PASSWORD\', \'\'),', $appConfigFile); - } + $this->assertStringContainsString('\'default\' => env(\'DB_CONNECTION\', \'mysql\')', $appConfigFile); + $this->assertStringContainsString('\'port\' => env(\'DB_PORT\', 3306),', $appConfigFile); + // Both the following configurations had values in the original config, they should be stripped out once + // the .env file is generated. + $this->assertStringContainsString('\'username\' => env(\'DB_USERNAME\', \'\'),', $appConfigFile); + $this->assertStringContainsString('\'password\' => env(\'DB_PASSWORD\', \'\'),', $appConfigFile); } - protected function tearDown() + protected function tearDown(): void { $this->tearDownConfigFixtures(); $this->restoreEnvFile(); diff --git a/tests/unit/system/traits/AssetMakerTest.php b/tests/unit/system/traits/AssetMakerTest.php index 352d66107..606df9562 100644 --- a/tests/unit/system/traits/AssetMakerTest.php +++ b/tests/unit/system/traits/AssetMakerTest.php @@ -10,7 +10,7 @@ class AssetMakerTest extends TestCase { private $stub; - public function setUp() + public function setUp() : void { $this->createApplication(); $this->stub = new AssetMakerStub();