diff --git a/.drone.yml b/.drone.yml index 894e5a9d3ede6..6617cee6950f8 100644 --- a/.drone.yml +++ b/.drone.yml @@ -53,8 +53,15 @@ steps: - ./libraries/vendor/bin/phpcs --extensions=php -p --standard=ruleset.xml . - echo $(date) + - name: phan + image: joomlaprojects/docker-images:php7.4-ast + depends_on: [ phpcs ] + failure: ignore + commands: + - ./libraries/vendor/bin/phan + - name: npm - image: node:16-alpine + image: node:16-bullseye-slim depends_on: [ phpcs ] volumes: - name: npm-cache @@ -195,135 +202,117 @@ steps: commands: - npm run lint:js - - name: prepare_codeception_tests + - name: prepare_system_tests depends_on: - npm image: joomlaprojects/docker-images:systemtests + volumes: + - name: cypress-cache + path: /root/.cache/Cypress commands: - - sed -i 's/tests\\/Codeception\\/_output/\\/drone\\/src\\/tests\\/Codeception\\/_output/' codeception.yml + - sed -i 's/tests\\/Codeception\\/_output/\\/drone\\/src\\/tests\\/cypress\\/output/' codeception.yml - php libraries/vendor/bin/codecept build + - npx cypress install + - npx cypress verify - name: phpmin-api-mysql depends_on: - - prepare_codeception_tests + - prepare_system_tests image: joomlaprojects/docker-images:systemtests environment: JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1 commands: - - bash tests/Codeception/drone-api-run.sh "$(pwd)" mysql + - bash tests/Codeception/drone-api-run.sh "$(pwd)" mysql mysqli mysql jos_ - - name: phpmax-api-mysql + - name: phpmin-api-postgres depends_on: - - prepare_codeception_tests - image: joomlaprojects/docker-images:systemtests8.1 + - prepare_system_tests + image: joomlaprojects/docker-images:systemtests environment: JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1 commands: - - bash tests/Codeception/drone-api-run.sh "$(pwd)" mysqlphpmax - -# - name: phpnext-api-mysql -# depends_on: -# - phpmax-api-mysql -# image: joomlaprojects/docker-images:systemtests8.2 -# failure: ignore -# environment: -# JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1 -# commands: -# - bash tests/Codeception/drone-api-run.sh "$(pwd)" mysqlphpnext + - bash tests/Codeception/drone-api-run.sh "$(pwd)" postgres pgsql postgres jos_ - - name: phpmin-api-postgres + - name: phpmax-api-mysql depends_on: - - prepare_codeception_tests - image: joomlaprojects/docker-images:systemtests + - phpmin-api-mysql + - phpmin-api-postgres + image: joomlaprojects/docker-images:systemtests8.1 environment: JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1 commands: - - bash tests/Codeception/drone-api-run.sh "$(pwd)" postgres + - bash tests/Codeception/drone-api-run.sh "$(pwd)" mysqlphpmax mysqli mysql phpmax_ - name: phpmax-api-postgres depends_on: - - prepare_codeception_tests + - phpmin-api-mysql + - phpmin-api-postgres image: joomlaprojects/docker-images:systemtests8.1 environment: JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1 commands: - - bash tests/Codeception/drone-api-run.sh "$(pwd)" postgresphpmax - -# - name: phpnext-api-postgres -# depends_on: -# - phpmin-api-postgres -# image: joomlaprojects/docker-images:systemtests8.2 -# failure: ignore -# environment: -# JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1 -# commands: -# - bash tests/Codeception/drone-api-run.sh "$(pwd)" postgresphpnext + - bash tests/Codeception/drone-api-run.sh "$(pwd)" postgresphpmax pgsql postgres phpmax_ - name: phpmin-system-mysql depends_on: - phpmax-api-mysql - phpmax-api-postgres image: joomlaprojects/docker-images:cypress + volumes: + - name: cypress-cache + path: /root/.cache/Cypress environment: JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1 commands: - bash tests/cypress/drone-system-run.sh "$(pwd)" cmysql mysqli mysql - - name: phpmax-system-mysql + - name: phpmin-system-postgres depends_on: - phpmax-api-mysql - phpmax-api-postgres - image: joomlaprojects/docker-images:cypress8.1 + image: joomlaprojects/docker-images:cypress + volumes: + - name: cypress-cache + path: /root/.cache/Cypress environment: JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1 commands: - - bash tests/cypress/drone-system-run.sh "$(pwd)" cmysqlmax mysqli mysql - -# - name: phpnext-system-mysql -# depends_on: -# - phpmax-system-mysql -# image: joomlaprojects/docker-images:systemtests8.2 -# failure: ignore -# environment: -# JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1 -# commands: -# - bash tests/Codeception/drone-system-run.sh "$(pwd)" mysqlphpnext + - bash tests/cypress/drone-system-run.sh "$(pwd)" cpostgres pgsql postgres - - name: phpmin-system-postgres + - name: phpmax-system-mysql depends_on: - - phpmax-api-mysql - - phpmax-api-postgres - image: joomlaprojects/docker-images:cypress + - phpmin-system-mysql + - phpmin-system-postgres + image: joomlaprojects/docker-images:cypress8.1 + volumes: + - name: cypress-cache + path: /root/.cache/Cypress environment: JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1 commands: - - bash tests/cypress/drone-system-run.sh "$(pwd)" cpostgres pgsql postgres + - bash tests/cypress/drone-system-run.sh "$(pwd)" cmysqlmax mysqli mysql - name: phpmax-system-postgres depends_on: - - phpmax-api-mysql - - phpmax-api-postgres + - phpmin-system-mysql + - phpmin-system-postgres image: joomlaprojects/docker-images:cypress8.1 + volumes: + - name: cypress-cache + path: /root/.cache/Cypress environment: JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1 commands: - bash tests/cypress/drone-system-run.sh "$(pwd)" cpostgresmax pgsql postgres -# - name: phpnext-system-postgres -# depends_on: -# - phpmax-system-postgres -# image: joomlaprojects/docker-images:systemtests8.2 -# failure: ignore -# environment: -# JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1 -# commands: -# - bash tests/Codeception/drone-system-run.sh "$(pwd)" postgresphpnext - - name: phpmin-system-mysql8 depends_on: - phpmax-system-mysql - phpmax-system-postgres image: joomlaprojects/docker-images:cypress + volumes: + - name: cypress-cache + path: /root/.cache/Cypress environment: JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1 commands: @@ -334,35 +323,23 @@ steps: - phpmax-system-mysql - phpmax-system-postgres image: joomlaprojects/docker-images:cypress8.1 + volumes: + - name: cypress-cache + path: /root/.cache/Cypress environment: JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1 commands: - bash tests/cypress/drone-system-run.sh "$(pwd)" cmysql8max mysqli mysql8 -# - name: phpnext-system-mysql8 -# depends_on: -# - phpmax-system-mysql8 -# image: joomlaprojects/docker-images:systemtests8.2 -# failure: ignore -# environment: -# JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1 -# commands: -# - bash tests/Codeception/drone-system-run.sh "$(pwd)" mysql8phpnext - - name: artifacts-system-tests image: cschlosser/drone-ftps depends_on: - # - phpnext-system-mysql - # - phpnext-system-mysql8 - # - phpnext-system-postgres - phpmax-system-mysql - phpmax-system-mysql8 - phpmax-system-postgres - phpmin-system-mysql - phpmin-system-mysql8 - phpmin-system-postgres - # - phpnext-api-mysql - # - phpnext-api-postgres - phpmax-api-mysql - phpmax-api-postgres - phpmin-api-mysql @@ -389,6 +366,9 @@ volumes: - name: composer-cache host: path: /tmp/composer-cache + - name: cypress-cache + host: + path: /tmp/cypress-cache - name: npm-cache host: path: /tmp/npm-cache @@ -517,6 +497,6 @@ trigger: --- kind: signature -hmac: c850e892dfbe03549fe506a436dcafe7880cff3c78e9a3130582abbe7c13c3f9 +hmac: 824f98a6e4e05eea6d469af34ea3c9f9a47649b4fdda22438df692e3958d60fe ... diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 32e34f654da80..4954021a38ac0 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -16,5 +16,10 @@ Pull Request for Issue # . -### Documentation Changes Required +### Link to documentations +Please select: +- [ ] Documentation link for docs.joomla.org: +- [ ] No documentation changes for docs.joomla.org needed +- [ ] Pull Request link for manual.joomla.org: +- [ ] No documentation changes for manual.joomla.org needed diff --git a/.phan/config.php b/.phan/config.php new file mode 100644 index 0000000000000..e8873f0ff8752 --- /dev/null +++ b/.phan/config.php @@ -0,0 +1,357 @@ + '7.2', + + // If enabled, missing properties will be created when + // they are first seen. If false, we'll report an + // error message if there is an attempt to write + // to a class property that wasn't explicitly + // defined. + 'allow_missing_properties' => true, + + // If enabled, null can be cast to any type and any + // type can be cast to null. Setting this to true + // will cut down on false positives. + 'null_casts_as_any_type' => true, + + // If enabled, allow null to be cast as any array-like type. + // + // This is an incremental step in migrating away from `null_casts_as_any_type`. + // If `null_casts_as_any_type` is true, this has no effect. + 'null_casts_as_array' => true, + + // If enabled, allow any array-like type to be cast to null. + // This is an incremental step in migrating away from `null_casts_as_any_type`. + // If `null_casts_as_any_type` is true, this has no effect. + 'array_casts_as_null' => true, + + // If enabled, scalars (int, float, bool, string, null) + // are treated as if they can cast to each other. + // This does not affect checks of array keys. See `scalar_array_key_cast`. + 'scalar_implicit_cast' => true, + + // If enabled, any scalar array keys (int, string) + // are treated as if they can cast to each other. + // E.g. `array` can cast to `array` and vice versa. + // Normally, a scalar type such as int could only cast to/from int and mixed. + 'scalar_array_key_cast' => true, + + // If this has entries, scalars (int, float, bool, string, null) + // are allowed to perform the casts listed. + // + // E.g. `['int' => ['float', 'string'], 'float' => ['int'], 'string' => ['int'], 'null' => ['string']]` + // allows casting null to a string, but not vice versa. + // (subset of `scalar_implicit_cast`) + 'scalar_implicit_partial' => [], + + // If enabled, Phan will warn if **any** type in a method invocation's object + // is definitely not an object, + // or if **any** type in an invoked expression is not a callable. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_method_checking' => false, + + // If enabled, Phan will warn if **any** type of the object expression for a property access + // does not contain that property. + 'strict_object_checking' => false, + + // If enabled, Phan will warn if **any** type in the argument's union type + // cannot be cast to a type in the parameter's expected union type. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_param_checking' => false, + + // If enabled, Phan will warn if **any** type in a property assignment's union type + // cannot be cast to a type in the property's declared union type. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_property_checking' => false, + + // If enabled, Phan will warn if **any** type in a returned value's union type + // cannot be cast to the declared return type. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_return_checking' => false, + + // If true, seemingly undeclared variables in the global + // scope will be ignored. + // + // This is useful for projects with complicated cross-file + // globals that you have no hope of fixing. + 'ignore_undeclared_variables_in_global_scope' => true, + + // Set this to false to emit `PhanUndeclaredFunction` issues for internal functions that Phan has signatures for, + // but aren't available in the codebase, or from Reflection. + // (may lead to false positives if an extension isn't loaded) + // + // If this is true(default), then Phan will not warn. + // + // Even when this is false, Phan will still infer return values and check parameters of internal functions + // if Phan has the signatures. + 'ignore_undeclared_functions_with_known_signatures' => true, + + // Backwards Compatibility Checking. This is slow + // and expensive, but you should consider running + // it before upgrading your version of PHP to a + // new version that has backward compatibility + // breaks. + // + // If you are migrating from PHP 5 to PHP 7, + // you should also look into using + // [php7cc (no longer maintained)](https://github.com/sstalle/php7cc) + // and [php7mar](https://github.com/Alexia/php7mar), + // which have different backwards compatibility checks. + // + // If you are still using versions of php older than 5.6, + // `PHP53CompatibilityPlugin` may be worth looking into if you are not running + // syntax checks for php 5.3 through another method such as + // `InvokePHPNativeSyntaxCheckPlugin` (see .phan/plugins/README.md). + 'backward_compatibility_checks' => false, + + // If true, check to make sure the return type declared + // in the doc-block (if any) matches the return type + // declared in the method signature. + 'check_docblock_signature_return_type_match' => true, + + // This setting maps case-insensitive strings to union types. + // + // This is useful if a project uses phpdoc that differs from the phpdoc2 standard. + // + // If the corresponding value is the empty string, + // then Phan will ignore that union type (E.g. can ignore 'the' in `@return the value`) + // + // If the corresponding value is not empty, + // then Phan will act as though it saw the corresponding UnionTypes(s) + // when the keys show up in a UnionType of `@param`, `@return`, `@var`, `@property`, etc. + // + // This matches the **entire string**, not parts of the string. + // (E.g. `@return the|null` will still look for a class with the name `the`, but `@return the` will be ignored with the below setting) + // + // (These are not aliases, this setting is ignored outside of doc comments). + // (Phan does not check if classes with these names exist) + // + // Example setting: `['unknown' => '', 'number' => 'int|float', 'char' => 'string', 'long' => 'int', 'the' => '']` + 'phpdoc_type_mapping' => [], + + // Set to true in order to attempt to detect dead + // (unreferenced) code. Keep in mind that the + // results will only be a guess given that classes, + // properties, constants and methods can be referenced + // as variables (like `$class->$property` or + // `$class->$method()`) in ways that we're unable + // to make sense of. + // + // To more aggressively detect dead code, + // you may want to set `dead_code_detection_prefer_false_negative` to `false`. + 'dead_code_detection' => false, + + // Set to true in order to attempt to detect unused variables. + // `dead_code_detection` will also enable unused variable detection. + // + // This has a few known false positives, e.g. for loops or branches. + 'unused_variable_detection' => false, + + // Set to true in order to attempt to detect redundant and impossible conditions. + // + // This has some false positives involving loops, + // variables set in branches of loops, and global variables. + 'redundant_condition_detection' => false, + + // If enabled, Phan will act as though it's certain of real return types of a subset of internal functions, + // even if those return types aren't available in reflection (real types were taken from php 7.3 or 8.0-dev, depending on target_php_version). + // + // Note that with php 7 and earlier, php would return null or false for many internal functions if the argument types or counts were incorrect. + // As a result, enabling this setting with target_php_version 8.0 may result in false positives for `--redundant-condition-detection` when codebases also support php 7.x. + 'assume_real_types_for_internal_functions' => false, + + // If true, this runs a quick version of checks that takes less + // time at the cost of not running as thorough + // of an analysis. You should consider setting this + // to true only when you wish you had more **undiagnosed** issues + // to fix in your code base. + // + // In quick-mode the scanner doesn't rescan a function + // or a method's code block every time a call is seen. + // This means that the problem here won't be detected: + // + // ```php + // false, + + // Override to hardcode existence and types of (non-builtin) globals in the global scope. + // Class names should be prefixed with `\`. + // + // (E.g. `['_FOO' => '\FooClass', 'page' => '\PageClass', 'userId' => 'int']`) + 'globals_type_map' => [], + + // The minimum severity level to report on. This can be + // set to `Issue::SEVERITY_LOW`, `Issue::SEVERITY_NORMAL` or + // `Issue::SEVERITY_CRITICAL`. Setting it to only + // critical issues is a good place to start on a big + // sloppy mature code base. + 'minimum_severity' => Issue::SEVERITY_NORMAL, + + // Add any issue types (such as `'PhanUndeclaredMethod'`) + // to this list to inhibit them from being reported. + 'suppress_issue_types' => ['PhanDeprecatedClass', 'PhanUndeclaredConstant','PhanDeprecatedFunction'], + + // A regular expression to match files to be excluded + // from parsing and analysis and will not be read at all. + // + // This is useful for excluding groups of test or example + // directories/files, unanalyzable files, or files that + // can't be removed for whatever reason. + // (e.g. `'@Test\.php$@'`, or `'@vendor/.*/(tests|Tests)/@'`) + 'exclude_file_regex' => '@^vendor/.*/(tests?|Tests?)/@', + + // A list of files that will be excluded from parsing and analysis + // and will not be read at all. + // + // This is useful for excluding hopelessly unanalyzable + // files that can't be removed for whatever reason. + 'exclude_file_list' => [ + 'administrator\components\com_joomlaupdate\finalisation.php' + ], + + // A directory list that defines files that will be excluded + // from static analysis, but whose class and method + // information should be included. + // + // Generally, you'll want to include the directories for + // third-party code (such as "vendor/") in this list. + // + // n.b.: If you'd like to parse but not analyze 3rd + // party code, directories containing that code + // should be added to the `directory_list` as well as + // to `exclude_analysis_directory_list`. + 'exclude_analysis_directory_list' => [ + 'libraries/vendor/', + 'libraries/php-encryption', + 'libraries/phpass' + ], + + // Enable this to enable checks of require/include statements referring to valid paths. + // The settings `include_paths` and `warn_about_relative_include_statement` affect the checks. + 'enable_include_path_checks' => false, + + // The number of processes to fork off during the analysis + // phase. + 'processes' => 1, + + // List of case-insensitive file extensions supported by Phan. + // (e.g. `['php', 'html', 'htm']`) + 'analyzed_file_extensions' => [ + 'php', + ], + + // You can put paths to stubs of internal extensions in this config option. + // If the corresponding extension is **not** loaded, then Phan will use the stubs instead. + // Phan will continue using its detailed type annotations, + // but load the constants, classes, functions, and classes (and their Reflection types) + // from these stub files (doubling as valid php files). + // Use a different extension from php to avoid accidentally loading these. + // The `tools/make_stubs` script can be used to generate your own stubs (compatible with php 7.0+ right now) + // + // (e.g. `['xdebug' => '.phan/internal_stubs/xdebug.phan_php']`) + 'autoload_internal_extension_signatures' => [ + 'redis' => '.phan/redis.phan_php', + 'memcached' => '.phan/memcached.phan_php', + 'dom' => '.phan/dom.phan_php', + ], + + // A list of plugin files to execute. + // + // Plugins which are bundled with Phan can be added here by providing their name (e.g. `'AlwaysReturnPlugin'`) + // + // Documentation about available bundled plugins can be found [here](https://github.com/phan/phan/tree/v4/.phan/plugins). + // + // Alternately, you can pass in the full path to a PHP file with the plugin's implementation (e.g. `'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php'`) + 'plugins' => [], + + // A list of directories that should be parsed for class and + // method information. After excluding the directories + // defined in `exclude_analysis_directory_list`, the remaining + // files will be statically analyzed for errors. + // + // Thus, both first-party and third-party code being used by + // your application should be included in this list. + 'directory_list' => [ + 'libraries', + 'libraries/vendor/joomla', + 'administrator', + 'components', + 'plugins' + ], + + // A list of individual files to include in analysis + // with a path relative to the root directory of the + // project. + 'file_list' => [], +]; diff --git a/.phan/dom.phan_php b/.phan/dom.phan_php new file mode 100644 index 0000000000000..ebb92e84dd117 --- /dev/null +++ b/.phan/dom.phan_php @@ -0,0 +1,418 @@ +item->id); } - if (Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) { + if ( + Associations::isEnabled() && + ComponentHelper::isEnabled('com_associations') && + AssociationsHelper::hasSupport($component) + ) { ToolbarHelper::custom('category.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); } } diff --git a/administrator/components/com_contact/src/Table/ContactTable.php b/administrator/components/com_contact/src/Table/ContactTable.php index 85c248abbd28c..95f74cabab5b4 100644 --- a/administrator/components/com_contact/src/Table/ContactTable.php +++ b/administrator/components/com_contact/src/Table/ContactTable.php @@ -119,8 +119,13 @@ public function store($updateNulls = true) $table = Table::getInstance('ContactTable', __NAMESPACE__ . '\\', array('dbo' => $this->getDbo())); if ($table->load(array('alias' => $this->alias, 'catid' => $this->catid)) && ($table->id != $this->id || $this->id == 0)) { + // Is the existing contact trashed? $this->setError(Text::_('COM_CONTACT_ERROR_UNIQUE_ALIAS')); + if ($table->published === -2) { + $this->setError(Text::_('COM_CONTACT_ERROR_UNIQUE_ALIAS_TRASHED')); + } + return false; } diff --git a/administrator/components/com_content/src/Field/Modal/ArticleField.php b/administrator/components/com_content/src/Field/Modal/ArticleField.php index 995187559c289..a497aecd62789 100644 --- a/administrator/components/com_content/src/Field/Modal/ArticleField.php +++ b/administrator/components/com_content/src/Field/Modal/ArticleField.php @@ -123,6 +123,9 @@ protected function getInput() } catch (\RuntimeException $e) { Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); } + if (empty($title)) { + $value = ''; + } } $title = empty($title) ? Text::_('COM_CONTENT_SELECT_AN_ARTICLE') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); diff --git a/administrator/components/com_cpanel/src/View/Cpanel/HtmlView.php b/administrator/components/com_cpanel/src/View/Cpanel/HtmlView.php index 520d7704f9ca9..58df45b02ea30 100644 --- a/administrator/components/com_cpanel/src/View/Cpanel/HtmlView.php +++ b/administrator/components/com_cpanel/src/View/Cpanel/HtmlView.php @@ -10,8 +10,8 @@ namespace Joomla\Component\Cpanel\Administrator\View\Cpanel; -use Joomla\CMS\Application\ApplicationHelper; use Joomla\CMS\Factory; +use Joomla\CMS\Filter\OutputFilter; use Joomla\CMS\Helper\ModuleHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; @@ -61,7 +61,7 @@ public function display($tpl = null) $app = Factory::getApplication(); $dashboard = $app->input->getCmd('dashboard', ''); - $position = ApplicationHelper::stringURLSafe($dashboard); + $position = OutputFilter::stringURLSafe($dashboard); // Generate a title for the view cpanel if (!empty($dashboard)) { diff --git a/administrator/components/com_fields/src/Helper/FieldsHelper.php b/administrator/components/com_fields/src/Helper/FieldsHelper.php index ecfdefdd20345..99cd4d3ec17ce 100644 --- a/administrator/components/com_fields/src/Helper/FieldsHelper.php +++ b/administrator/components/com_fields/src/Helper/FieldsHelper.php @@ -19,6 +19,7 @@ use Joomla\CMS\Object\CMSObject; use Joomla\CMS\Plugin\PluginHelper; use Joomla\Component\Fields\Administrator\Model\FieldsModel; +use Joomla\Component\Fields\Administrator\Model\FieldModel; use Joomla\Database\ParameterType; // phpcs:disable PSR1.Files.SideEffects @@ -38,7 +39,7 @@ class FieldsHelper private static $fieldsCache = null; /** - * @var FieldsModel + * @var FieldModel */ private static $fieldCache = null; diff --git a/administrator/components/com_fields/src/Model/FieldModel.php b/administrator/components/com_fields/src/Model/FieldModel.php index 5ae40ea68c880..c0ff23176a82d 100644 --- a/administrator/components/com_fields/src/Model/FieldModel.php +++ b/administrator/components/com_fields/src/Model/FieldModel.php @@ -452,6 +452,8 @@ protected function generateNewTitle($categoryId, $name, $title) */ public function delete(&$pks) { + $db = $this->getDatabase(); + $success = parent::delete($pks); if ($success) { @@ -461,20 +463,20 @@ public function delete(&$pks) if (!empty($pks)) { // Delete Values - $query = $this->getDatabase()->getQuery(true); + $query = $db->getQuery(true); - $query->delete($query->quoteName('#__fields_values')) - ->whereIn($query->quoteName('field_id'), $pks); + $query->delete($db->quoteName('#__fields_values')) + ->whereIn($db->quoteName('field_id'), $pks); - $this->getDatabase()->setQuery($query)->execute(); + $db->setQuery($query)->execute(); // Delete Assigned Categories - $query = $this->getDatabase()->getQuery(true); + $query = $db->getQuery(true); - $query->delete($query->quoteName('#__fields_categories')) - ->whereIn($query->quoteName('field_id'), $pks); + $query->delete($db->quoteName('#__fields_categories')) + ->whereIn($db->quoteName('field_id'), $pks); - $this->getDatabase()->setQuery($query)->execute(); + $db->setQuery($query)->execute(); } } @@ -620,15 +622,16 @@ public function setFieldValue($fieldId, $itemId, $value) $fieldId = (int) $fieldId; // Deleting the existing record as it is a reset - $query = $this->getDatabase()->getQuery(true); + $db = $this->getDatabase(); + $query = $db->getQuery(true); - $query->delete($query->quoteName('#__fields_values')) - ->where($query->quoteName('field_id') . ' = :fieldid') - ->where($query->quoteName('item_id') . ' = :itemid') + $query->delete($db->quoteName('#__fields_values')) + ->where($db->quoteName('field_id') . ' = :fieldid') + ->where($db->quoteName('item_id') . ' = :itemid') ->bind(':fieldid', $fieldId, ParameterType::INTEGER) ->bind(':itemid', $itemId); - $this->getDatabase()->setQuery($query)->execute(); + $db->setQuery($query)->execute(); } if ($needsInsert) { @@ -703,16 +706,17 @@ public function getFieldValues(array $fieldIds, $itemId) // Fill the cache when it doesn't exist if (!array_key_exists($key, $this->valueCache)) { // Create the query - $query = $this->getDatabase()->getQuery(true); + $db = $this->getDatabase(); + $query = $db->getQuery(true); - $query->select($query->quoteName(['field_id', 'value'])) - ->from($query->quoteName('#__fields_values')) - ->whereIn($query->quoteName('field_id'), ArrayHelper::toInteger($fieldIds)) - ->where($query->quoteName('item_id') . ' = :itemid') + $query->select($db->quoteName(['field_id', 'value'])) + ->from($db->quoteName('#__fields_values')) + ->whereIn($db->quoteName('field_id'), ArrayHelper::toInteger($fieldIds)) + ->where($db->quoteName('item_id') . ' = :itemid') ->bind(':itemid', $itemId); // Fetch the row from the database - $rows = $this->getDatabase()->setQuery($query)->loadObjectList(); + $rows = $db->setQuery($query)->loadObjectList(); $data = array(); @@ -757,20 +761,21 @@ public function getFieldValues(array $fieldIds, $itemId) public function cleanupValues($context, $itemId) { // Delete with inner join is not possible so we need to do a subquery - $fieldsQuery = $this->getDatabase()->getQuery(true); - $fieldsQuery->select($fieldsQuery->quoteName('id')) - ->from($fieldsQuery->quoteName('#__fields')) - ->where($fieldsQuery->quoteName('context') . ' = :context'); + $db = $this->getDatabase(); + $fieldsQuery = $db->getQuery(true); + $fieldsQuery->select($db->quoteName('id')) + ->from($db->quoteName('#__fields')) + ->where($db->quoteName('context') . ' = :context'); - $query = $this->getDatabase()->getQuery(true); + $query = $db->getQuery(true); - $query->delete($query->quoteName('#__fields_values')) - ->where($query->quoteName('field_id') . ' IN (' . $fieldsQuery . ')') - ->where($query->quoteName('item_id') . ' = :itemid') + $query->delete($db->quoteName('#__fields_values')) + ->where($db->quoteName('field_id') . ' IN (' . $fieldsQuery . ')') + ->where($db->quoteName('item_id') . ' = :itemid') ->bind(':itemid', $itemId) ->bind(':context', $context); - $this->getDatabase()->setQuery($query)->execute(); + $db->setQuery($query)->execute(); } /** diff --git a/administrator/components/com_finder/src/Controller/DisplayController.php b/administrator/components/com_finder/src/Controller/DisplayController.php index 820f6b65f36ba..ec4905231e7d2 100644 --- a/administrator/components/com_finder/src/Controller/DisplayController.php +++ b/administrator/components/com_finder/src/Controller/DisplayController.php @@ -71,7 +71,7 @@ public function display($cachable = false, $urlparams = array()) if ($view === 'filter' && $layout === 'edit' && !$this->checkEditId('com_finder.edit.filter', $filterId)) { // Somehow the person just went to the form - we don't allow that. if (!\count($this->app->getMessageQueue())) { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $f_id), 'error'); + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $filterId), 'error'); } $this->setRedirect(Route::_('index.php?option=com_finder&view=filters', false)); diff --git a/administrator/components/com_finder/src/Indexer/Query.php b/administrator/components/com_finder/src/Indexer/Query.php index 0e220ddc6a2da..a5bea020c25b0 100644 --- a/administrator/components/com_finder/src/Indexer/Query.php +++ b/administrator/components/com_finder/src/Indexer/Query.php @@ -1245,8 +1245,8 @@ protected function getTokenData($token) $searchTerm = $token->term; $searchStem = $token->stem; - $term = $query->quoteName('t.term'); - $stem = $query->quoteName('t.stem'); + $term = $db->quoteName('t.term'); + $stem = $db->quoteName('t.stem'); if ($this->wordmode === 'begin') { $searchTerm .= '%'; diff --git a/administrator/components/com_finder/src/Indexer/Result.php b/administrator/components/com_finder/src/Indexer/Result.php index 4d7e9abce813b..6a067a76df219 100644 --- a/administrator/components/com_finder/src/Indexer/Result.php +++ b/administrator/components/com_finder/src/Indexer/Result.php @@ -394,6 +394,11 @@ public function getTaxonomy($branch = null) */ public function addTaxonomy($branch, $title, $state = 1, $access = 1, $language = '') { + // We can't add taxonomies with empty titles + if (!trim($title)) { + return; + } + // Filter the input. $branch = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,_]+#mui', ' ', $branch); @@ -424,6 +429,11 @@ public function addTaxonomy($branch, $title, $state = 1, $access = 1, $language */ public function addNestedTaxonomy($branch, ImmutableNodeInterface $contentNode, $state = 1, $access = 1, $language = '') { + // We can't add taxonomies with empty titles + if (!trim($contentNode->title)) { + return; + } + // Filter the input. $branch = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,_]+#mui', ' ', $branch); diff --git a/administrator/components/com_finder/src/Table/FilterTable.php b/administrator/components/com_finder/src/Table/FilterTable.php index 621076fa65ccb..932fb30caf88a 100644 --- a/administrator/components/com_finder/src/Table/FilterTable.php +++ b/administrator/components/com_finder/src/Table/FilterTable.php @@ -157,7 +157,7 @@ public function store($updateNulls = true) $table = new static($this->getDbo()); if ($table->load(array('alias' => $this->alias)) && ($table->filter_id != $this->filter_id || $this->filter_id == 0)) { - $this->setError(Text::_('JLIB_DATABASE_ERROR_ARTICLE_UNIQUE_ALIAS')); + $this->setError(Text::_('COM_FINDER_FILTER_ERROR_UNIQUE_ALIAS')); return false; } diff --git a/administrator/components/com_languages/tmpl/overrides/default.php b/administrator/components/com_languages/tmpl/overrides/default.php index 954a765486f3e..872e4e2e30b28 100644 --- a/administrator/components/com_languages/tmpl/overrides/default.php +++ b/administrator/components/com_languages/tmpl/overrides/default.php @@ -93,11 +93,11 @@ - diff --git a/administrator/components/com_media/resources/scripts/components/browser/actionItems/actionItemsContainer.vue b/administrator/components/com_media/resources/scripts/components/browser/actionItems/actionItemsContainer.vue index f22bb8daa2662..0694262cab894 100644 --- a/administrator/components/com_media/resources/scripts/components/browser/actionItems/actionItemsContainer.vue +++ b/administrator/components/com_media/resources/scripts/components/browser/actionItems/actionItemsContainer.vue @@ -17,107 +17,129 @@ @on-focused="focused" @keyup.up="openLastActions()" @keyup.down="openActions()" + @keyup.end="openLastActions()" + @keyup.home="openActions()" + @keydown.up.prevent + @keydown.down.prevent + @keydown.home.prevent + @keydown.end.prevent /> @@ -170,6 +192,7 @@ export default { /* Hide actions dropdown */ hideActions() { this.showActions = false; + this.$parent.$parent.$data.actionsActive = false; }, /* Preview an item */ openPreview() { @@ -200,19 +223,89 @@ export default { /* Open actions dropdown */ openActions() { this.showActions = true; + this.$parent.$parent.$data.actionsActive = true; const buttons = [...this.$el.parentElement.querySelectorAll('.media-browser-actions-list button')]; if (buttons.length) { + buttons.forEach((button, i) => { + if (i === (0)) { + button.tabIndex = 0; + } else { + button.tabIndex = -1; + } + }); buttons[0].focus(); } }, /* Open actions dropdown and focus on last element */ openLastActions() { this.showActions = true; + this.$parent.$parent.$data.actionsActive = true; const buttons = [...this.$el.parentElement.querySelectorAll('.media-browser-actions-list button')]; if (buttons.length) { + buttons.forEach((button, i) => { + if (i === (buttons.length)) { + button.tabIndex = 0; + } else { + button.tabIndex = -1; + } + }); this.$nextTick(() => buttons[buttons.length - 1].focus()); } }, + /* Focus on the next item or go to the beginning again */ + focusNext(event) { + const active = event.target; + const buttons = [...active.parentElement.querySelectorAll('button')]; + const lastchild = buttons[buttons.length - 1]; + active.tabIndex = -1; + if (active === lastchild) { + buttons[0].focus(); + buttons[0].tabIndex = 0; + } else { + active.nextElementSibling.focus(); + active.nextElementSibling.tabIndex = 0; + } + }, + /* Focus on the previous item or go to the end again */ + focusPrev(event) { + const active = event.target; + const buttons = [...active.parentElement.querySelectorAll('button')]; + const firstchild = buttons[0]; + active.tabIndex = -1; + if (active === firstchild) { + buttons[buttons.length - 1].focus(); + buttons[buttons.length - 1].tabIndex = 0; + } else { + active.previousElementSibling.focus(); + active.previousElementSibling.tabIndex = 0; + } + }, + /* Focus on the first item */ + focusFirst(event) { + const active = event.target; + const buttons = [...active.parentElement.querySelectorAll('button')]; + buttons[0].focus(); + buttons.forEach((button, i) => { + if (i === 0) { + button.tabIndex = 0; + } else { + button.tabIndex = -1; + } + }); + }, + /* Focus on the last item */ + focusLast(event) { + const active = event.target; + const buttons = [...active.parentElement.querySelectorAll('button')]; + buttons[buttons.length - 1].focus(); + buttons.forEach((button, i) => { + if (i === (buttons.length)) { + button.tabIndex = 0; + } else { + button.tabIndex = -1; + } + }); + }, editItem() { this.edit(); }, diff --git a/administrator/components/com_media/resources/scripts/components/browser/actionItems/delete.vue b/administrator/components/com_media/resources/scripts/components/browser/actionItems/delete.vue index 70008116b1b82..ceb05b83e2daa 100644 --- a/administrator/components/com_media/resources/scripts/components/browser/actionItems/delete.vue +++ b/administrator/components/com_media/resources/scripts/components/browser/actionItems/delete.vue @@ -2,19 +2,20 @@ diff --git a/administrator/components/com_media/resources/scripts/components/browser/actionItems/download.vue b/administrator/components/com_media/resources/scripts/components/browser/actionItems/download.vue index 914964d1d57f4..dc973f93333f3 100644 --- a/administrator/components/com_media/resources/scripts/components/browser/actionItems/download.vue +++ b/administrator/components/com_media/resources/scripts/components/browser/actionItems/download.vue @@ -2,10 +2,9 @@ diff --git a/administrator/components/com_media/resources/scripts/components/browser/actionItems/edit.vue b/administrator/components/com_media/resources/scripts/components/browser/actionItems/edit.vue index ca796ea12c5e2..0c82cf4bf0b68 100644 --- a/administrator/components/com_media/resources/scripts/components/browser/actionItems/edit.vue +++ b/administrator/components/com_media/resources/scripts/components/browser/actionItems/edit.vue @@ -2,10 +2,9 @@ diff --git a/administrator/components/com_media/resources/scripts/components/browser/actionItems/preview.vue b/administrator/components/com_media/resources/scripts/components/browser/actionItems/preview.vue index db29e51b300ff..e74f3ec84590c 100644 --- a/administrator/components/com_media/resources/scripts/components/browser/actionItems/preview.vue +++ b/administrator/components/com_media/resources/scripts/components/browser/actionItems/preview.vue @@ -2,8 +2,7 @@ diff --git a/administrator/components/com_media/resources/scripts/components/browser/actionItems/rename.vue b/administrator/components/com_media/resources/scripts/components/browser/actionItems/rename.vue index b2ef7221c889f..bf89a55886c53 100644 --- a/administrator/components/com_media/resources/scripts/components/browser/actionItems/rename.vue +++ b/administrator/components/com_media/resources/scripts/components/browser/actionItems/rename.vue @@ -3,8 +3,7 @@ ref="actionRenameButton" type="button" class="action-rename" - :aria-label="translate('COM_MEDIA_ACTION_RENAME')" - :title="translate('COM_MEDIA_ACTION_RENAME')" + @click.stop="openRenameModal()" @keyup.enter="openRenameModal()" @keyup.space="openRenameModal()" @focus="focused(true)" @@ -12,10 +11,12 @@ @keyup.esc="hideActions()" >